diff --git a/app/build.gradle b/app/build.gradle index 32ac7f20a..07eff54e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java b/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java index 9173cec05..975c6f97f 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java @@ -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); } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/database/NetworkLanguageDao.java b/app/src/main/java/org/kiwix/kiwixmobile/database/NetworkLanguageDao.java index 31bc63626..b63e3bcf2 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/database/NetworkLanguageDao.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/database/NetworkLanguageDao.java @@ -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> activeLanguageProcessor = - BehaviorProcessor.create(); private final BehaviorProcessor> 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> activeLanguages() { - return activeLanguageProcessor; - } - public Flowable> allLanguages() { return allLanguageProcessor; } @@ -61,10 +53,6 @@ public class NetworkLanguageDao extends BaseDao { return fetchWith(Query.select()); } - public List fetchActiveLanguages() { - return fetchWith(Query.select().where(NetworkLanguageDatabaseEntity.ENABLED.eq(true))); - } - @NotNull private List fetchWith(Query query) { ArrayList result = new ArrayList<>(); final NetworkLanguageDatabaseEntity databaseEntity = diff --git a/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadViewHolder.kt b/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadViewHolder.kt index 1ae781514..021e7329e 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadViewHolder.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadViewHolder.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadItem.kt index 91b7f1aeb..9e36c78a0 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadItem.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadItem.kt @@ -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 ) } \ No newline at end of file diff --git a/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadStatus.kt b/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadStatus.kt index 0cf4e6caf..745741c3b 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadStatus.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadStatus.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt index e334bc2c2..3752797ff 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt @@ -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 } ) .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>, @@ -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) = library + private fun updateLanguagesInDao( + library: Flowable + ) = 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, - activeLanguages: List + allLanguages: List ): List { - val languageCounts = booksFromNetwork.mapNotNull { it.language } + val networkLanguageCounts = booksFromNetwork.mapNotNull { it.language } .fold( mutableMapOf(), - { 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 MutableMap.increment(key: K) = + apply { set(key, getOrElse(key, { 0 }) + 1) } + + private fun fromLocalesWithNetworkMatchesSetActiveBy( + networkLanguageCounts: MutableMap, + listToActivateBy: List + ): List { 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, - 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, + locale: Locale + ) = allLanguages.firstOrNull { it.languageCode == locale.isO3Language }?.active == true private fun combineLibrarySources( booksOnFileSystem: List, activeDownloads: List, - activeLanguages: List, + allLanguages: List, libraryNetworkEntity: LibraryNetworkEntity, filter: String ): List { 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 @@ -340,4 +366,6 @@ class ZimManageViewModel @Inject constructor( .map(downloader::queryStatus) .distinctUntilChanged() -} \ No newline at end of file +} + + diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml index 59ff51528..6c1f14ea1 100644 --- a/app/src/main/res/layout/download_item.xml +++ b/app/src/main/res/layout/download_item.xml @@ -58,12 +58,12 @@ /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 589291922..e3ce85a5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -181,4 +181,23 @@ Device Details SEND DETAILS NO THANKS + Pending + In Progress + Complete + Paused: %s + Failed: %s + Waiting for Wifi + Waiting to connect to a network + Waiting to retry + Unknown + Cannot resume due to unknown reason + Could not find storage + File already exists + Unknown file system error + HTTP data error + Not enough space on storage + Too many redirects + Unknown HTTP code received + Unknown + HTTP code %s