diff --git a/app/objectbox-models/default.json.bak b/app/objectbox-models/default.json.bak index d51d2ba30..757048b9f 100644 --- a/app/objectbox-models/default.json.bak +++ b/app/objectbox-models/default.json.bak @@ -218,7 +218,7 @@ }, { "id": "2:6862771806221961183", - "name": "zimID" + "name": "zimId" }, { "id": "3:4312769031500860715", diff --git a/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadModel.kt index 8a343797c..85d8277e2 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/downloader/model/DownloadModel.kt @@ -18,10 +18,13 @@ package org.kiwix.kiwixmobile.downloader.model import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book +import org.kiwix.kiwixmobile.utils.StorageUtils data class DownloadModel( val databaseId: Long? = null, - val downloadId: Long , + val downloadId: Long, val book: Book -) +) { + val fileNameFromUrl: String get() = StorageUtils.getFileNameFromUrl(book.url) +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java index a618bd884..2d54aacf5 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java @@ -37,7 +37,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.CountDownTimer; -import android.os.Environment; import android.os.Handler; import android.provider.Settings; import android.text.SpannableString; @@ -84,10 +83,10 @@ import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; +import io.reactivex.android.schedulers.AndroidSchedulers; import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -100,20 +99,18 @@ import org.kiwix.kiwixmobile.base.BaseActivity; import org.kiwix.kiwixmobile.bookmark.BookmarkItem; import org.kiwix.kiwixmobile.bookmark.BookmarksActivity; import org.kiwix.kiwixmobile.data.ZimContentProvider; -import org.kiwix.kiwixmobile.data.local.entity.Bookmark; import org.kiwix.kiwixmobile.help.HelpActivity; import org.kiwix.kiwixmobile.history.HistoryActivity; import org.kiwix.kiwixmobile.history.HistoryListItem; -import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity; import org.kiwix.kiwixmobile.search.SearchActivity; import org.kiwix.kiwixmobile.settings.KiwixSettingsActivity; import org.kiwix.kiwixmobile.utils.DimenUtils; import org.kiwix.kiwixmobile.utils.LanguageUtils; import org.kiwix.kiwixmobile.utils.NetworkUtils; import org.kiwix.kiwixmobile.utils.StyleUtils; -import org.kiwix.kiwixmobile.utils.files.FileSearch; import org.kiwix.kiwixmobile.utils.files.FileUtils; import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity; +import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver; import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate; import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskAdapter; import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem; @@ -201,14 +198,18 @@ public class MainActivity extends BaseActivity implements WebViewCallback, ImageView bottomToolbarArrowBack; @BindView(R.id.bottom_toolbar_arrow_forward) ImageView bottomToolbarArrowForward; - @Inject - MainContract.Presenter presenter; @BindView(R.id.tab_switcher_recycler_view) RecyclerView tabRecyclerView; @BindView(R.id.activity_main_tab_switcher) View tabSwitcherRoot; @BindView(R.id.tab_switcher_close_all_tabs) FloatingActionButton closeAllTabsButton; + + @Inject + MainContract.Presenter presenter; + @Inject + StorageObserver storageObserver; + private CountDownTimer hideBackToTopTimer = new CountDownTimer(1200, 1200) { @Override public void onTick(long millisUntilFinished) { @@ -274,21 +275,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback, closeTab(viewHolder.getAdapterPosition()); } }; - private FileSearch fileSearch = - new FileSearch(this, Collections.emptyList(), new FileSearch.ResultListener() { - final List newBooks = new ArrayList<>(); - @Override public void onBookFound(BooksOnDiskListItem.BookOnDisk bookOnDisk) { - runOnUiThread(() -> { - newBooks.add(bookOnDisk); - }); - } - - @Override - public void onScanCompleted() { - presenter.saveBooks(newBooks); - } - }); private static void updateWidgets(Context context) { Intent intent = new Intent(context.getApplicationContext(), KiwixSearchWidget.class); @@ -739,7 +726,6 @@ public class MainActivity extends BaseActivity implements WebViewCallback, downloadBookButton = null; hideBackToTopTimer.cancel(); hideBackToTopTimer = null; - fileSearch = null; // TODO create a base Activity class that class this. FileUtils.deleteCachedFiles(this); tts.shutdown(); @@ -1199,7 +1185,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback, case REQUEST_READ_STORAGE_PERMISSION: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - fileSearch.scan(sharedPreferenceUtil.getPrefStorage()); + scanStorageForZims(); } else { Snackbar.make(drawerLayout, R.string.request_storage, Snackbar.LENGTH_LONG) .setAction(R.string.menu_settings, view -> { @@ -1228,6 +1214,13 @@ public class MainActivity extends BaseActivity implements WebViewCallback, } } + private void scanStorageForZims() { + storageObserver.getBooksOnFileSystem() + .take(1) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(presenter::saveBooks, Throwable::printStackTrace); + } + // Workaround for popup bottom menu on older devices private void StyleMenuButtons(Menu m) { // Find each menu item and set its text colour @@ -2116,7 +2109,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback, new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, REQUEST_READ_STORAGE_PERMISSION); } else { - fileSearch.scan(sharedPreferenceUtil.getPrefStorage()); + scanStorageForZims(); } } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.java b/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.java deleted file mode 100644 index ae0610420..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2013 Rashiq Ahmad - * - * 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 - * 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, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301, USA. - */ - -package org.kiwix.kiwixmobile.utils.files; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Environment; -import android.provider.MediaStore; -import android.util.Log; -import eu.mhutti1.utils.storage.StorageDevice; -import eu.mhutti1.utils.storage.StorageDeviceUtils; -import java.io.File; -import java.io.FilenameFilter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Vector; -import org.kiwix.kiwixmobile.data.ZimContentProvider; -import org.kiwix.kiwixmobile.downloader.model.DownloadModel; -import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity; -import org.kiwix.kiwixmobile.utils.StorageUtils; -import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem; - -import static org.kiwix.kiwixmobile.utils.Constants.TAG_KIWIX; - -public class FileSearch { - - // Array of zim file extensions - public static final String[] zimFiles = { "zim", "zimaa" }; - - private final Context context; - private final List downloads; - private final ResultListener listener; - - private boolean fileSystemScanCompleted = false; - private boolean mediaStoreScanCompleted = false; - - public FileSearch(Context ctx, List downloads, ResultListener listener) { - this.context = ctx; - this.downloads = downloads; - this.listener = listener; - } - - public static synchronized LibraryNetworkEntity.Book fileToBook(String filePath) { - LibraryNetworkEntity.Book book = null; - - if (ZimContentProvider.zimFileName != null) { - ZimContentProvider.originalFileName = ZimContentProvider.zimFileName; - } - // Check a file isn't being opened and temporally use content provider to access details - // This is not a great solution as we shouldn't need to fully open our ZIM files to get their metadata - if (ZimContentProvider.canIterate) { - if (ZimContentProvider.setZimFile(filePath) != null) { - try { - book = new LibraryNetworkEntity.Book(); - book.title = ZimContentProvider.getZimFileTitle(); - book.id = ZimContentProvider.getId(); - book.file = new File(filePath); - book.size = String.valueOf(ZimContentProvider.getFileSize()); - book.favicon = ZimContentProvider.getFavicon(); - book.creator = ZimContentProvider.getCreator(); - book.publisher = ZimContentProvider.getPublisher(); - book.date = ZimContentProvider.getDate(); - book.description = ZimContentProvider.getDescription(); - book.language = ZimContentProvider.getLanguage(); - } catch (Exception e) { - // TODO 20171215 Consider more elegant approaches. - // This is to see if we can catch the exception at all! - Log.e("kiwix-filesearch", "Problem parsing a book entry from the library file. ", e); - return null; - } - } - } - // Return content provider to its previous state - if (!ZimContentProvider.originalFileName.equals("")) { - ZimContentProvider.setZimFile(ZimContentProvider.originalFileName); - } - ZimContentProvider.originalFileName = ""; - - return book; - } - - public void scan(String defaultPath) { - // Start custom file search - new Thread(() -> { - scanFileSystem(defaultPath); - fileSystemScanCompleted = true; - checkCompleted(); - }).start(); - - // Star mediastore search - new Thread(() -> { - scanMediaStore(); - mediaStoreScanCompleted = true; - checkCompleted(); - }).start(); - } - - // If both searches are complete callback - private synchronized void checkCompleted() { - if (mediaStoreScanCompleted && fileSystemScanCompleted) { - listener.onScanCompleted(); - } - } - - public void scanMediaStore() { - ContentResolver contentResolver = context.getContentResolver(); - Uri uri = MediaStore.Files.getContentUri("external"); - - String[] projection = { MediaStore.MediaColumns.DATA }; - String selection = - MediaStore.MediaColumns.DATA + " like ? or " + MediaStore.MediaColumns.DATA + " like ? "; - - Cursor query = contentResolver.query(uri, projection, selection, - new String[] { "%." + zimFiles[0], "%." + zimFiles[1] }, null); - - if (query == null) { - return; - } - - try { - while (query.moveToNext()) { - File file = new File(query.getString(0)); - if (file.canRead()) { - onFileFound(file.getAbsolutePath()); - } - } - } finally { - query.close(); - } - } - - // Scan through the file system and find all the files with .zim and .zimaa extensions - public void scanFileSystem(String defaultPath) { - FilenameFilter[] filter = new FilenameFilter[zimFiles.length]; - - // Search all external directories that we can find. - final ArrayList storageDevices = - StorageDeviceUtils.getStorageDevices(context, false); - String[] tempRoots = new String[storageDevices.size() + 2]; - int j = 0; - tempRoots[j++] = "/mnt"; - tempRoots[j++] = defaultPath; - for (StorageDevice storageDevice : storageDevices) { - tempRoots[j++] = storageDevice.getName(); - } - - int i = 0; - for (final String extension : zimFiles) { - filter[i] = (dir, name) -> name.endsWith("." + extension); - i++; - } - - String dirNamePrimary = new File( - Environment.getExternalStorageDirectory().getAbsolutePath()).toString(); - - for (final String dirName : tempRoots) { - if (dirNamePrimary.equals(dirName)) { - // We already got this directory from getExternalStorageDirectory(). - continue; - } - File f = new File(dirName); - if (f.isDirectory()) { - scanDirectory(dirName, filter); - } else { - Log.i(TAG_KIWIX, "Skipping missing directory " + dirName); - } - } - } - - // Iterate through the file system - private Collection listFiles(File directory, FilenameFilter[] filter, int recurse) { - - Vector files = new Vector<>(); - - File[] entries = directory.listFiles(); - - if (entries != null) { - for (File entry : entries) { - for (FilenameFilter filefilter : filter) { - if (filter == null || filefilter.accept(directory, entry.getName())) { - files.add(entry); - } - } - if ((recurse <= -1) || (recurse > 0 && entry.isDirectory())) { - recurse--; - files.addAll(listFiles(entry, filter, recurse)); - recurse++; - } - } - } - return files; - } - - private File[] listFilesAsArray(File directory, FilenameFilter[] filter, int recurse) { - Collection files = listFiles(directory, filter, recurse); - - File[] arr = new File[files.size()]; - return files.toArray(arr); - } - - public static synchronized BooksOnDiskListItem.BookOnDisk fileToBookOnDisk(String filePath) { - LibraryNetworkEntity.Book book = null; - - if (ZimContentProvider.zimFileName != null) { - ZimContentProvider.originalFileName = ZimContentProvider.zimFileName; - } - // Check a file isn't being opened and temporally use content provider to access details - // This is not a great solution as we shouldn't need to fully open our ZIM files to get their metadata - if (ZimContentProvider.canIterate) { - if (ZimContentProvider.setZimFile(filePath) != null) { - try { - book = new LibraryNetworkEntity.Book(); - book.title = ZimContentProvider.getZimFileTitle(); - book.id = ZimContentProvider.getId(); - book.size = String.valueOf(ZimContentProvider.getFileSize()); - book.favicon = ZimContentProvider.getFavicon(); - book.creator = ZimContentProvider.getCreator(); - book.publisher = ZimContentProvider.getPublisher(); - book.date = ZimContentProvider.getDate(); - book.description = ZimContentProvider.getDescription(); - book.language = ZimContentProvider.getLanguage(); - } catch (Exception e) { - // TODO 20171215 Consider more elegant approaches. - // This is to see if we can catch the exception at all! - Log.e("kiwix-filesearch", "Problem parsing a book entry from the library file. ", e); - return null; - } - } - } - // Return content provider to its previous state - if (!ZimContentProvider.originalFileName.equals("")) { - ZimContentProvider.setZimFile(ZimContentProvider.originalFileName); - } - ZimContentProvider.originalFileName = ""; - - return book == null ? null - : new BooksOnDiskListItem.BookOnDisk(null, book, new File(filePath),0L); - } - - // Fill fileList with files found in the specific directory - private void scanDirectory(String directory, FilenameFilter[] filter) { - Log.d(TAG_KIWIX, "Searching directory " + directory); - File[] foundFiles = listFilesAsArray(new File(directory), filter, -1); - for (File f : foundFiles) { - Log.d(TAG_KIWIX, "Found " + f.getAbsolutePath()); - onFileFound(f.getAbsolutePath()); - } - } - - // Callback that a new file has been found - public void onFileFound(String filePath) { - if (fileIsDownloading(filePath)) { - return; - } - BooksOnDiskListItem.BookOnDisk book = fileToBookOnDisk(filePath); - - if (book != null) { - listener.onBookFound(book); - } - } - - private boolean fileIsDownloading(String filePath) { - for (DownloadModel download : downloads) { - if (filePath.endsWith(StorageUtils.getFileNameFromUrl(download.getBook().getUrl()))) { - return true; - } - } - return false; - } - - public interface ResultListener { - void onBookFound(BooksOnDiskListItem.BookOnDisk book); - - void onScanCompleted(); - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt b/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt new file mode 100644 index 000000000..2da77e83b --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2013 Rashiq Ahmad + * + * 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 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.kiwix.kiwixmobile.utils.files + +import android.content.Context +import android.os.Environment +import android.provider.MediaStore.Files +import android.provider.MediaStore.MediaColumns +import eu.mhutti1.utils.storage.StorageDeviceUtils +import io.reactivex.Flowable +import io.reactivex.functions.BiFunction +import org.kiwix.kiwixmobile.extensions.forEachRow +import org.kiwix.kiwixmobile.extensions.get +import java.io.File +import javax.inject.Inject + +class FileSearch @Inject constructor(private val context: Context) { + + val zimFileExtensions = arrayOf("zim", "zimaa") + + fun scan(defaultPath: String) = + Flowable.combineLatest( + Flowable.fromCallable { scanFileSystem(defaultPath) }, + Flowable.fromCallable(this::scanMediaStore), + BiFunction, List, List> { filesSystemFiles, mediaStoreFiles -> + mutableListOf().apply { + addAll(filesSystemFiles) + addAll(mediaStoreFiles) + } + } + ) + + private fun scanMediaStore() = mutableListOf().apply { + queryMediaStore() + ?.forEachRow { cursor -> + File(cursor.get(MediaColumns.DATA)).takeIf(File::canRead) + ?.also { add(it) } + } + } + + private fun queryMediaStore() = context.contentResolver + .query( + Files.getContentUri("external"), + arrayOf(MediaColumns.DATA), + MediaColumns.DATA + " like ? or " + MediaColumns.DATA + " like ? ", + arrayOf("%." + zimFileExtensions[0], "%." + zimFileExtensions[1]), + null + ) + + private fun scanFileSystem(defaultPath: String) = + directoryRoots(defaultPath) + .minus(Environment.getExternalStorageDirectory().absolutePath) + .fold(mutableListOf(), { acc, root -> + if (File(root).isDirectory) acc.addAll(scanDirectory(root)) + acc + }) + + private fun directoryRoots(defaultPath: String) = listOf( + "/mnt", + defaultPath, + *StorageDeviceUtils.getStorageDevices(context, false).map { it.name }.toTypedArray() + ) + + private fun scanDirectory(directory: String) = filesMatchingExtensions(directory) ?: emptyList() + + private fun filesMatchingExtensions(directory: String) = File(directory) + .listFiles { dir, name -> name?.endsWithAny(*zimFileExtensions) ?: false } + ?.toList() + +} + +private fun String.endsWithAny(vararg suffixes: String) = + suffixes.fold(false, {acc, s -> acc or endsWith(s) }) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt index a33eb39f5..f04614e3b 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt @@ -1,45 +1,87 @@ package org.kiwix.kiwixmobile.zim_manager.fileselect_view -import android.content.Context import android.util.Log -import io.reactivex.processors.PublishProcessor +import io.reactivex.functions.BiFunction import io.reactivex.schedulers.Schedulers +import org.kiwix.kiwixmobile.data.ZimContentProvider import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao import org.kiwix.kiwixmobile.downloader.model.DownloadModel +import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.utils.files.FileSearch -import org.kiwix.kiwixmobile.utils.files.FileSearch.ResultListener import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk +import java.io.File import javax.inject.Inject class StorageObserver @Inject constructor( - private val context: Context, private val sharedPreferenceUtil: SharedPreferenceUtil, - private val downloadDao: NewDownloadDao + downloadDao: NewDownloadDao, + private val fileSearch: FileSearch ) { - private val _booksOnFileSystem = PublishProcessor.create>() - val booksOnFileSystem = _booksOnFileSystem.distinctUntilChanged() - .doOnSubscribe { - downloadDao.downloads() - .subscribeOn(Schedulers.io()) - .take(1) - .subscribe(this::scanFiles, Throwable::printStackTrace) + val booksOnFileSystem = scanFiles() + .withLatestFrom( + downloadDao.downloads(), + BiFunction(this::toFilesThatAreNotDownloading) + ) + .map { + it.mapNotNull { file -> convertToBookOnDisk(file) } } - private fun scanFiles(downloads: List) { - FileSearch(context, downloads, object : ResultListener { - val foundBooks = mutableSetOf() + private fun toFilesThatAreNotDownloading( + files: List, + downloads: List + ) = files.filter { fileHasNoMatchingDownload(downloads, it) } - override fun onBookFound(book: BookOnDisk) { - foundBooks.add(book) - Log.i("Scanner", "File Search: Found Book " + book.book.title) + private fun fileHasNoMatchingDownload( + downloads: List, + file: File + ) = downloads.firstOrNull { + file.absolutePath.endsWith(it.fileNameFromUrl) + } == null + + private fun scanFiles() = fileSearch.scan(sharedPreferenceUtil.prefStorage) + .subscribeOn(Schedulers.io()) + + private fun convertToBookOnDisk(file: File): BookOnDisk? { + configureZimContentProvider() + if (ZimContentProvider.canIterate && ZimContentProvider.setZimFile(file.absolutePath) != null) { + try { + return BookOnDisk(book = bookFromZimContentProvider(), file = file) + } catch (e: Exception) { + // TODO 20171215 Consider more elegant approaches. + // This is to see if we can catch the exception at all! + Log.e("kiwix-filesearch", "Problem parsing a book entry from the library file. ", e) + } finally { + resetZimContentProvider() } + } + return null + } - override fun onScanCompleted() { - _booksOnFileSystem.onNext(foundBooks.toList()) + private fun bookFromZimContentProvider() = Book().apply { + title = ZimContentProvider.getZimFileTitle() + id = ZimContentProvider.getId() + size = ZimContentProvider.getFileSize() + .toString() + favicon = ZimContentProvider.getFavicon() + creator = ZimContentProvider.getCreator() + publisher = ZimContentProvider.getPublisher() + date = ZimContentProvider.getDate() + description = ZimContentProvider.getDescription() + language = ZimContentProvider.getLanguage() + } - } - }).scan(sharedPreferenceUtil.prefStorage) + private fun resetZimContentProvider() { + if (ZimContentProvider.originalFileName != "") { + ZimContentProvider.setZimFile(ZimContentProvider.originalFileName) + } + ZimContentProvider.originalFileName = "" + } + + private fun configureZimContentProvider() { + if (ZimContentProvider.zimFileName != null) { + ZimContentProvider.originalFileName = ZimContentProvider.zimFileName + } } } diff --git a/app/src/test/java/org/kiwix/kiwixmobile/TestModelFunctions.kt b/app/src/test/java/org/kiwix/kiwixmobile/TestModelFunctions.kt index f2071a281..8e620aff5 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/TestModelFunctions.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/TestModelFunctions.kt @@ -27,11 +27,31 @@ import java.io.File fun bookOnDisk( book: Book = book(), - databaseId: Long = 0L, + databaseId: Long? = 0L, file: File = File("") ) = BookOnDisk(databaseId, book, file) -fun book(id: String = "0") = Book().apply { this.id = id } +fun book( + id: String = "0", + title: String = "", + size: String = "", + favicon: String = "", + creator: String = "", + publisher: String = "", + date: String = "", + description: String = "", + language: String = "" +) = Book().apply { + this.id = id + this.title = title + this.size = size + this.favicon = favicon + this.creator = creator + this.publisher = publisher + this.date = date + this.description = description + this.language = language +} fun downloadStatus( downloadId: Long = 0L, diff --git a/app/src/test/java/org/kiwix/kiwixmobile/TestUtilitiyFunctions.kt b/app/src/test/java/org/kiwix/kiwixmobile/TestUtilitiyFunctions.kt new file mode 100644 index 000000000..1f7029941 --- /dev/null +++ b/app/src/test/java/org/kiwix/kiwixmobile/TestUtilitiyFunctions.kt @@ -0,0 +1,34 @@ +/* + * Kiwix Android + * Copyright (C) 2018 Kiwix + * + * 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 . + */ +package org.kiwix.kiwixmobile + +import io.reactivex.Scheduler +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins + +fun setScheduler(replacementScheduler: Scheduler) { + RxJavaPlugins.setIoSchedulerHandler { scheduler -> replacementScheduler } + RxJavaPlugins.setComputationSchedulerHandler { scheduler -> replacementScheduler } + RxJavaPlugins.setNewThreadSchedulerHandler { scheduler -> replacementScheduler } + RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> replacementScheduler } +} + +fun resetSchedulers() { + RxJavaPlugins.reset() + RxAndroidPlugins.reset() +} diff --git a/app/src/test/java/org/kiwix/kiwixmobile/utils/files/FileSearchTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/utils/files/FileSearchTest.kt new file mode 100644 index 000000000..a76949f85 --- /dev/null +++ b/app/src/test/java/org/kiwix/kiwixmobile/utils/files/FileSearchTest.kt @@ -0,0 +1,139 @@ +/* + * Kiwix Android + * Copyright (C) 2018 Kiwix + * + * 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 . + */ + +package org.kiwix.kiwixmobile.utils.files + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.os.Environment +import android.provider.MediaStore.MediaColumns +import eu.mhutti1.utils.storage.StorageDevice +import eu.mhutti1.utils.storage.StorageDeviceUtils +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.io.File + +class FileSearchTest { + + private val context: Context = mockk() + private lateinit var fileSearch: FileSearch + + private val externalStorageDirectory: File = mockk() + private val contentResolver: ContentResolver = mockk() + private val storageDevice: StorageDevice = mockk() + + private val unitTestTempDirectoryPath = "unittest${File.separator}" + + @BeforeEach + fun init() { + clearMocks(context, externalStorageDirectory, contentResolver, storageDevice) + deleteTempDirectory() + mockkStatic(StorageDeviceUtils::class) + mockkStatic(Environment::class) + every { Environment.getExternalStorageDirectory() } returns externalStorageDirectory + every { externalStorageDirectory.absolutePath } returns "/externalStorageDirectory" + every { context.contentResolver } returns contentResolver + every { StorageDeviceUtils.getStorageDevices(context, false) } returns arrayListOf( + storageDevice + ) + every { storageDevice.name } returns "/deviceDir" + fileSearch = FileSearch(context) + } + + @AfterAll + fun teardown() { + deleteTempDirectory() + } + + @Nested + inner class FileSystem { + + @Test + fun `scan of directory that doesn't exist returns nothing`() { + every { contentResolver.query(any(), any(), any(), any(), any()) } returns null + fileSearch.scan("doesNotExist") + .test() + .assertValue(listOf()) + } + + @Test + fun `scan of directory that has files returns files`() { + val zimFile = File.createTempFile("${unitTestTempDirectoryPath}fileToFind", ".zim") + val zimaaFile = File.createTempFile("${unitTestTempDirectoryPath}fileToFind2", ".zimaa") + File.createTempFile("${unitTestTempDirectoryPath}willNotFind", ".txt") + every { contentResolver.query(any(), any(), any(), any(), any()) } returns null + val fileList = fileSearch.scan(zimFile.parent) + .test() + .values()[0] + assertThat(fileList).containsExactlyInAnyOrder(zimFile, zimaaFile) + } + } + + @Nested + inner class MediaStore { + + @Test + fun `scan media store, if files are readable they are returned`() { + val fileToFind = File.createTempFile("${unitTestTempDirectoryPath}fileToFind", ".zim") + expectFromMediaStore(fileToFind) + fileSearch.scan("") + .test() + .assertValue(listOf(fileToFind)) + } + + @Test + fun `scan media store, if files are not readable they are not returned`() { + val unreadableFile = File.createTempFile("${unitTestTempDirectoryPath}fileToFind", ".zim") + expectFromMediaStore(unreadableFile) + unreadableFile.delete() + fileSearch.scan("") + .test() + .assertValue(listOf()) + } + + private fun expectFromMediaStore(fileToFind: File) { + val cursor = mockk() + every { + contentResolver.query( + null, + arrayOf(MediaColumns.DATA), + MediaColumns.DATA + " like ? or " + MediaColumns.DATA + " like ? ", + arrayOf("%." + "zim", "%." + "zimaa"), + null + ) + } returns cursor + every { cursor.moveToNext() } returnsMany listOf(true, false) + every { cursor.columnNames } returns arrayOf(MediaColumns.DATA) + every { cursor.getColumnIndex(MediaColumns.DATA) } returns 0 + every { cursor.getString(0) } returns fileToFind.absolutePath + } + } + + private fun deleteTempDirectory() { + File.createTempFile("${unitTestTempDirectoryPath}temp", ".txt") + .parentFile.deleteRecursively() + } +} diff --git a/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModelTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModelTest.kt index 59b81cca0..ab4a70896 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModelTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModelTest.kt @@ -20,16 +20,12 @@ package org.kiwix.kiwixmobile.zim_manager import android.app.Application import com.jraska.livedata.test -import io.mockk.clearMocks +import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import io.mockk.verify -import io.reactivex.Scheduler import io.reactivex.Single -import io.reactivex.android.plugins.RxAndroidPlugins -import io.reactivex.plugins.RxJavaPlugins import io.reactivex.processors.PublishProcessor -import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.TestScheduler import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeEach @@ -55,6 +51,8 @@ import org.kiwix.kiwixmobile.downloader.model.DownloadStatus import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book +import org.kiwix.kiwixmobile.resetSchedulers +import org.kiwix.kiwixmobile.setScheduler import org.kiwix.kiwixmobile.utils.BookUtils import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4GbFile @@ -104,26 +102,14 @@ class ZimManageViewModelTest { setScheduler(testScheduler) } - private fun setScheduler(replacementScheduler: Scheduler) { - RxJavaPlugins.setIoSchedulerHandler { scheduler -> replacementScheduler } - RxJavaPlugins.setComputationSchedulerHandler { scheduler -> replacementScheduler } - RxJavaPlugins.setNewThreadSchedulerHandler { scheduler -> replacementScheduler } - RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> Schedulers.trampoline() } - } - @AfterAll fun teardown() { - RxJavaPlugins.reset() - RxAndroidPlugins.reset() + resetSchedulers() } @BeforeEach fun init() { - clearMocks( - newDownloadDao, newBookDao, newLanguagesDao, downloader, - storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils, - fat32Checker, uriToFileConverter, defaultLanguageProvider, dataSource - ) + clearAllMocks() every { connectivityBroadcastReceiver.action } returns "test" every { newDownloadDao.downloads() } returns downloads every { newBookDao.books() } returns books diff --git a/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserverTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserverTest.kt new file mode 100644 index 000000000..b4070db8c --- /dev/null +++ b/app/src/test/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserverTest.kt @@ -0,0 +1,138 @@ +package org.kiwix.kiwixmobile.zim_manager.fileselect_view + +/* + * Kiwix Android + * Copyright (C) 2018 Kiwix + * + * 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 . + */ + +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.reactivex.processors.PublishProcessor +import io.reactivex.schedulers.Schedulers +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.kiwix.kiwixmobile.book +import org.kiwix.kiwixmobile.bookOnDisk +import org.kiwix.kiwixmobile.data.ZimContentProvider +import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao +import org.kiwix.kiwixmobile.downloader.model.DownloadModel +import org.kiwix.kiwixmobile.resetSchedulers +import org.kiwix.kiwixmobile.setScheduler +import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.utils.files.FileSearch +import java.io.File + +class StorageObserverTest { + + private val sharedPreferenceUtil: SharedPreferenceUtil = mockk() + private val newDownloadDao: NewDownloadDao = mockk() + private val fileSearch: FileSearch = mockk() + private val downloadModel = mockk() + private val file = mockk() + + private val files: PublishProcessor> = PublishProcessor.create() + private val downloads: PublishProcessor> = PublishProcessor.create() + + private lateinit var storageObserver: StorageObserver + + init { + setScheduler(Schedulers.trampoline()) + } + + @AfterAll + fun teardown() { + resetSchedulers() + } + + @BeforeEach fun init() { + clearAllMocks() + every { sharedPreferenceUtil.prefStorage } returns "a" + every { fileSearch.scan("a") } returns files + every { newDownloadDao.downloads() } returns downloads + storageObserver = StorageObserver(sharedPreferenceUtil, newDownloadDao, fileSearch) + } + + @Test + fun `books from disk are filtered by current downloads`() { + every { downloadModel.fileNameFromUrl } returns "test" + every { file.absolutePath } returns "This is a test" + storageObserver.booksOnFileSystem + .test() + .also { + downloads.offer(listOf(downloadModel)) + files.offer(listOf(file)) + } + .assertValues(listOf()) + } + + @Test + fun `null books from ZimContentProvider are filtered out`() { + every { downloadModel.fileNameFromUrl } returns "test" + every { file.absolutePath } returns "This won't match" + + storageObserver.booksOnFileSystem + .test() + .also { + downloads.offer(listOf(downloadModel)) + files.offer(listOf(file)) + } + .assertValues(listOf()) + } + + @Test + fun `iterable ZimContentProvider with zim file produces a book`() { + val expectedBook = book( + "id", "title", "1", "favicon", "creator", "publisher", "date", + "description", "language" + ) + mockkStatic(ZimContentProvider::class) + every { downloadModel.fileNameFromUrl } returns "test" + every { file.absolutePath } returns "This won't match" + + ZimContentProvider.canIterate = true + every { ZimContentProvider.setZimFile("This won't match") } returns "" + + every { ZimContentProvider.getZimFileTitle() } returns expectedBook.title + every { ZimContentProvider.getId() } returns expectedBook.id + every { ZimContentProvider.getFileSize() } returns expectedBook.size.toInt() + every { ZimContentProvider.getFavicon() } returns expectedBook.favicon + every { ZimContentProvider.getCreator() } returns expectedBook.creator + every { ZimContentProvider.getPublisher() } returns expectedBook.publisher + every { ZimContentProvider.getDate() } returns expectedBook.date + every { ZimContentProvider.getDescription() } returns expectedBook.description + every { ZimContentProvider.getLanguage() } returns expectedBook.language + + storageObserver.booksOnFileSystem + .test() + .also { + downloads.offer(listOf(downloadModel)) + files.offer(listOf(file)) + } + .assertValues( + listOf( + bookOnDisk( + book = expectedBook, + file = file, + databaseId = null + ) + + ) + ) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 06ec21335..27e70ee7f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,7 +35,7 @@ ext { set("powerMockVersion", "1.6.6") set("powerMockJUnitVersion", "1.7.4") set("baristaVersion", "2.7.1") - set("kotlinVersion", "1.3.31") + set("kotlinVersion", "1.3.40") set("objectboxVersion", "2.3.4") }