Add fat32 checker that checks on storage change and on deletion/file removal. Fix library stream breaking on error

This commit is contained in:
Sean Mac Gillicuddy 2019-05-14 14:19:46 +01:00
parent e5773391c9
commit 73690fbd23
6 changed files with 173 additions and 43 deletions

View File

@ -24,7 +24,6 @@ import com.yahoo.squidb.sql.Query;
import io.reactivex.Flowable; import io.reactivex.Flowable;
import io.reactivex.processors.BehaviorProcessor; import io.reactivex.processors.BehaviorProcessor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -42,7 +41,7 @@ public class NetworkLanguageDao extends BaseDao {
@Override @Override
protected void onUpdateToTable() { protected void onUpdateToTable() {
allLanguageProcessor.onNext(fetchAllLanguages()); allLanguageProcessor.onNext(fetchAllLanguages());
} }
public Flowable<List<Language>> allLanguages() { public Flowable<List<Language>> allLanguages() {
@ -75,21 +74,20 @@ public class NetworkLanguageDao extends BaseDao {
} }
public void saveFilteredLanguages(List<Language> languages) { public void saveFilteredLanguages(List<Language> languages) {
kiwixDatabase.beginTransaction(); if (languages.size() > 0) {
kiwixDatabase.deleteAll(NetworkLanguageDatabaseEntity.class); kiwixDatabase.beginTransaction();
Collections.sort(languages, kiwixDatabase.deleteAll(NetworkLanguageDatabaseEntity.class);
(language, t1) -> language.getLanguage().compareTo(t1.getLanguage())); for (int i = 0; i < languages.size(); i++) {
Language language = languages.get(i);
for (int i = 0; i < languages.size(); i++) { NetworkLanguageDatabaseEntity networkLanguageDatabaseEntity =
Language language = languages.get(i); new NetworkLanguageDatabaseEntity();
NetworkLanguageDatabaseEntity networkLanguageDatabaseEntity = networkLanguageDatabaseEntity.setLanguageISO3(language.getLanguageCode());
new NetworkLanguageDatabaseEntity(); networkLanguageDatabaseEntity.setIsEnabled(language.getActive());
networkLanguageDatabaseEntity.setLanguageISO3(language.getLanguageCode()); networkLanguageDatabaseEntity.setNumberOfOccurences(language.getOccurencesOfLanguage());
networkLanguageDatabaseEntity.setIsEnabled(language.getActive()); kiwixDatabase.persist(networkLanguageDatabaseEntity);
networkLanguageDatabaseEntity.setNumberOfOccurences(language.getOccurencesOfLanguage()); }
kiwixDatabase.persist(networkLanguageDatabaseEntity); kiwixDatabase.setTransactionSuccessful();
kiwixDatabase.endTransaction();
} }
kiwixDatabase.setTransactionSuccessful();
kiwixDatabase.endTransaction();
} }
} }

View File

@ -19,6 +19,7 @@ package org.kiwix.kiwixmobile.network;
import io.reactivex.Flowable; import io.reactivex.Flowable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity; import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity;
@ -30,7 +31,7 @@ import retrofit2.http.GET;
import retrofit2.http.Url; import retrofit2.http.Url;
public interface KiwixService { public interface KiwixService {
@GET("/library/library_zim.xml") Flowable<LibraryNetworkEntity> getLibrary(); @GET("/library/library_zim.xml") Single<LibraryNetworkEntity> getLibrary();
@GET Observable<MetaLinkNetworkEntity> getMetaLinks(@Url String url); @GET Observable<MetaLinkNetworkEntity> getMetaLinks(@Url String url);

View File

@ -4,7 +4,7 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import io.reactivex.processors.BehaviorProcessor;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -13,7 +13,6 @@ import static org.kiwix.kiwixmobile.utils.Constants.PREF_BACK_TO_TOP;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_BOTTOM_TOOLBAR; import static org.kiwix.kiwixmobile.utils.Constants.PREF_BOTTOM_TOOLBAR;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_EXTERNAL_LINK_POPUP; import static org.kiwix.kiwixmobile.utils.Constants.PREF_EXTERNAL_LINK_POPUP;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_FULLSCREEN; import static org.kiwix.kiwixmobile.utils.Constants.PREF_FULLSCREEN;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_FULL_TEXT_SEARCH;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_HIDE_TOOLBAR; import static org.kiwix.kiwixmobile.utils.Constants.PREF_HIDE_TOOLBAR;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_IS_FIRST_RUN; import static org.kiwix.kiwixmobile.utils.Constants.PREF_IS_FIRST_RUN;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_LANG; import static org.kiwix.kiwixmobile.utils.Constants.PREF_LANG;
@ -33,11 +32,13 @@ import static org.kiwix.kiwixmobile.utils.Constants.PREF_ZOOM_ENABLED;
public class SharedPreferenceUtil { public class SharedPreferenceUtil {
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor; private SharedPreferences.Editor editor;
public final BehaviorProcessor<String> prefStorages;
@Inject @Inject
public SharedPreferenceUtil(Context context) { public SharedPreferenceUtil(Context context) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
editor = sharedPreferences.edit(); editor = sharedPreferences.edit();
prefStorages = BehaviorProcessor.createDefault(getPrefStorage());
} }
public void remove(String key) { public void remove(String key) {
@ -131,6 +132,7 @@ public class SharedPreferenceUtil {
public void putPrefStorage(String storage) { public void putPrefStorage(String storage) {
editor.putString(PREF_STORAGE, storage).apply(); editor.putString(PREF_STORAGE, storage).apply();
prefStorages.onNext(storage);
} }
public void putPrefFullScreen(boolean fullScreen) { public void putPrefFullScreen(boolean fullScreen) {

View File

@ -0,0 +1,101 @@
/*
* Kiwix Android
* Copyright (C) 2018 Kiwix <android.kiwix.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kiwix.kiwixmobile.zim_manager
import android.os.FileObserver
import android.util.Log
import io.reactivex.Flowable
import io.reactivex.functions.BiFunction
import io.reactivex.processors.BehaviorProcessor
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
import java.io.File
import java.io.RandomAccessFile
import javax.inject.Inject
class Fat32Checker @Inject constructor(sharedPreferenceUtil: SharedPreferenceUtil) {
private val _fileSystemStates: BehaviorProcessor<FileSystemState> = BehaviorProcessor.create()
val fileSystemStates = _fileSystemStates.distinctUntilChanged()
var fileObserver: FileObserver? = null
private val requestCheckSystemFileType = BehaviorProcessor.createDefault(Unit)
init {
Flowable.combineLatest(
sharedPreferenceUtil.prefStorages.distinctUntilChanged(),
requestCheckSystemFileType,
BiFunction { storage: String, _: Unit -> storage }
)
.subscribe(
{
val systemState = toFileSystemState(it)
_fileSystemStates.onNext(systemState)
fileObserver = if (systemState == NotEnoughSpaceFor4GbFile) fileObserver(it) else null
},
Throwable::printStackTrace
)
}
private fun fileObserver(it: String?): FileObserver {
return object : FileObserver(it, MOVED_FROM or DELETE) {
override fun onEvent(
event: Int,
path: String?
) {
requestCheckSystemFileType.onNext(Unit)
}
}.apply { startWatching() }
}
private fun toFileSystemState(it: String) =
when {
File(it).freeSpace > FOUR_GIGABYTES ->
if (canCreate4GbFile(it)) CanWrite4GbFile
else CannotWrite4GbFile
else -> NotEnoughSpaceFor4GbFile
}
private fun canCreate4GbFile(storage: String): Boolean {
val path = "$storage/large_file_test.txt"
File(path).delete()
try {
RandomAccessFile(path, "rw").use {
it.setLength(FOUR_GIGABYTES)
return true
}
} catch (e: Exception) {
Log.d("Fat32Checker", e.message)
return false
} finally {
File(path).delete()
}
}
companion object {
const val FOUR_GIGABYTES = 4L * 1024L * 1024L * 1024L
const val FOUR_GIGABYTES_IN_KILOBYTES = 4L * 1024L * 1024L
}
sealed class FileSystemState() {
object NotEnoughSpaceFor4GbFile : FileSystemState()
object CanWrite4GbFile : FileSystemState()
object CannotWrite4GbFile : FileSystemState()
}
}

View File

@ -99,7 +99,9 @@ class ZimManageActivity : BaseActivity() {
LanguageSelectDialog.Builder(this, dialogStyle()) LanguageSelectDialog.Builder(this, dialogStyle())
.apply { .apply {
onOkClicked = { onOkClicked = {
Flowable.fromCallable { languagesDao.saveFilteredLanguages(it) } Flowable.fromCallable {
languagesDao.saveFilteredLanguages(it.sortedBy(Language::language))
}
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe() .subscribe()
} }

View File

@ -25,7 +25,7 @@ import io.reactivex.Flowable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.functions.Function5 import io.reactivex.functions.Function6
import io.reactivex.processors.BehaviorProcessor import io.reactivex.processors.BehaviorProcessor
import io.reactivex.processors.PublishProcessor import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
@ -44,6 +44,11 @@ import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.network.KiwixService import org.kiwix.kiwixmobile.network.KiwixService
import org.kiwix.kiwixmobile.utils.BookUtils import org.kiwix.kiwixmobile.utils.BookUtils
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver 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.Language
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
@ -64,7 +69,8 @@ class ZimManageViewModel @Inject constructor(
private val kiwixService: KiwixService, private val kiwixService: KiwixService,
private val context: Application, private val context: Application,
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver,
private val bookUtils: BookUtils private val bookUtils: BookUtils,
private val fat32Checker: Fat32Checker
) : ViewModel() { ) : ViewModel() {
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData() val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
@ -97,20 +103,44 @@ class ZimManageViewModel @Inject constructor(
val downloads: Flowable<MutableList<DownloadModel>> = downloadDao.downloads() val downloads: Flowable<MutableList<DownloadModel>> = downloadDao.downloads()
val downloadStatuses = downloadStatuses(downloads) val downloadStatuses = downloadStatuses(downloads)
val booksFromDao: Flowable<List<Book>> = books() val booksFromDao: Flowable<List<Book>> = books()
val library = libraryFromNetwork() val networkLibrary = PublishProcessor.create<LibraryNetworkEntity>()
return arrayOf( return arrayOf(
updateDownloadItems(downloadStatuses), updateDownloadItems(downloadStatuses),
removeCompletedDownloadsFromDb(downloadStatuses), removeCompletedDownloadsFromDb(downloadStatuses),
removeNonExistingDownloadsFromDb(downloadStatuses, downloads), removeNonExistingDownloadsFromDb(downloadStatuses, downloads),
updateBookItems(booksFromDao), updateBookItems(booksFromDao),
checkFileSystemForBooksOnRequest(booksFromDao), checkFileSystemForBooksOnRequest(booksFromDao),
updateLibraryItems(booksFromDao, downloads, library), updateLibraryItems(booksFromDao, downloads, networkLibrary),
updateLanguagesInDao(library), updateLanguagesInDao(networkLibrary),
updateNetworkStates(), updateNetworkStates(),
updateLanguageItemsForDialog() updateLanguageItemsForDialog(),
requestsAndConnectivtyChangesToLibraryRequests(networkLibrary)
) )
} }
private fun requestsAndConnectivtyChangesToLibraryRequests(library: PublishProcessor<LibraryNetworkEntity>) =
Flowable.combineLatest(
requestDownloadLibrary,
connectivityBroadcastReceiver.networkStates.distinctUntilChanged().filter(
CONNECTED::equals
),
BiFunction<Unit, NetworkState, Unit> { _, _ -> Unit }
)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
{
kiwixService.library.subscribe(
{ library.onNext(it) },
{
it.printStackTrace()
library.onNext(LibraryNetworkEntity().apply { book = LinkedList() })
}
)
},
Throwable::printStackTrace
)
private fun removeNonExistingDownloadsFromDb( private fun removeNonExistingDownloadsFromDb(
downloadStatuses: Flowable<List<DownloadStatus>>, downloadStatuses: Flowable<List<DownloadStatus>>,
downloads: Flowable<MutableList<DownloadModel>> downloads: Flowable<MutableList<DownloadModel>>
@ -164,20 +194,6 @@ class ZimManageViewModel @Inject constructor(
networkStates::postValue, Throwable::printStackTrace networkStates::postValue, Throwable::printStackTrace
) )
private fun libraryFromNetwork() =
Flowable.combineLatest(
requestDownloadLibrary,
connectivityBroadcastReceiver.networkStates.distinctUntilChanged().filter(
NetworkState.CONNECTED::equals
),
BiFunction<Unit, NetworkState, Unit> { _, _ -> Unit }
)
.subscribeOn(Schedulers.io())
.doOnNext { libraryListIsRefreshing.postValue(true) }
.switchMap { kiwixService.library }
.doOnError(Throwable::printStackTrace)
.onErrorResumeNext(Flowable.just(LibraryNetworkEntity().apply { book = LinkedList() }))
private fun updateLibraryItems( private fun updateLibraryItems(
booksFromDao: Flowable<List<Book>>, booksFromDao: Flowable<List<Book>>,
downloads: Flowable<MutableList<DownloadModel>>, downloads: Flowable<MutableList<DownloadModel>>,
@ -191,7 +207,8 @@ class ZimManageViewModel @Inject constructor(
.doOnNext { libraryListIsRefreshing.postValue(true) } .doOnNext { libraryListIsRefreshing.postValue(true) }
.debounce(500, MILLISECONDS) .debounce(500, MILLISECONDS)
.observeOn(Schedulers.io()), .observeOn(Schedulers.io()),
Function5(this::combineLibrarySources) fat32Checker.fileSystemStates,
Function6(this::combineLibrarySources)
) )
.doOnNext { libraryListIsRefreshing.postValue(false) } .doOnNext { libraryListIsRefreshing.postValue(false) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -209,6 +226,7 @@ class ZimManageViewModel @Inject constructor(
languageDao.allLanguages(), languageDao.allLanguages(),
BiFunction(this::combineToLanguageList) BiFunction(this::combineToLanguageList)
) )
.map { it.sortedBy(Language::language) }
.subscribe( .subscribe(
languageDao::saveFilteredLanguages, languageDao::saveFilteredLanguages,
Throwable::printStackTrace Throwable::printStackTrace
@ -274,7 +292,8 @@ class ZimManageViewModel @Inject constructor(
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
allLanguages: List<Language>, allLanguages: List<Language>,
libraryNetworkEntity: LibraryNetworkEntity, libraryNetworkEntity: LibraryNetworkEntity,
filter: String filter: String,
fileSystemState: FileSystemState
): List<LibraryListItem> { ): List<LibraryListItem> {
val downloadedBooksIds = booksOnFileSystem.map { it.id } val downloadedBooksIds = booksOnFileSystem.map { it.id }
val downloadingBookIds = activeDownloads.map { it.book.id } val downloadingBookIds = activeDownloads.map { it.book.id }
@ -283,6 +302,13 @@ class ZimManageViewModel @Inject constructor(
val booksUnfilteredByLanguage = val booksUnfilteredByLanguage =
applyUserFilter( applyUserFilter(
libraryNetworkEntity.books libraryNetworkEntity.books
.filter {
when (fileSystemState) {
CannotWrite4GbFile -> it.size.toLongOrNull() ?: 0L < Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES
NotEnoughSpaceFor4GbFile,
CanWrite4GbFile -> true
}
}
.filterNot { downloadedBooksIds.contains(it.id) } .filterNot { downloadedBooksIds.contains(it.id) }
.filterNot { downloadingBookIds.contains(it.id) } .filterNot { downloadingBookIds.contains(it.id) }
.filterNot { it.url.contains("/stack_exchange/") },// Temp filter see #694, filter) .filterNot { it.url.contains("/stack_exchange/") },// Temp filter see #694, filter)