#1238 Unit test FileSearch

This commit is contained in:
Sean Mac Gillicuddy 2019-06-28 14:18:16 +01:00
parent 0539d56849
commit fd9f797105
12 changed files with 516 additions and 367 deletions

View File

@ -218,7 +218,7 @@
},
{
"id": "2:6862771806221961183",
"name": "zimID"
"name": "zimId"
},
{
"id": "3:4312769031500860715",

View File

@ -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)
}

View File

@ -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<BooksOnDiskListItem.BookOnDisk> 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();
}
}

View File

@ -1,296 +0,0 @@
/*
* Copyright 2013 Rashiq Ahmad <rashiq.z@gmail.com>
*
* 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<DownloadModel> downloads;
private final ResultListener listener;
private boolean fileSystemScanCompleted = false;
private boolean mediaStoreScanCompleted = false;
public FileSearch(Context ctx, List<DownloadModel> 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<StorageDevice> 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<File> listFiles(File directory, FilenameFilter[] filter, int recurse) {
Vector<File> 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<File> 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();
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2013 Rashiq Ahmad <rashiq.z@gmail.com>
*
* 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<File>, List<File>, List<File>> { filesSystemFiles, mediaStoreFiles ->
mutableListOf<File>().apply {
addAll(filesSystemFiles)
addAll(mediaStoreFiles)
}
}
)
private fun scanMediaStore() = mutableListOf<File>().apply {
queryMediaStore()
?.forEachRow { cursor ->
File(cursor.get<String>(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<File>(), { 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) })

View File

@ -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<List<BookOnDisk>>()
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<DownloadModel>) {
FileSearch(context, downloads, object : ResultListener {
val foundBooks = mutableSetOf<BookOnDisk>()
private fun toFilesThatAreNotDownloading(
files: List<File>,
downloads: List<DownloadModel>
) = 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<DownloadModel>,
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
}
}
}

View File

@ -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,

View File

@ -0,0 +1,34 @@
/*
* 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
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()
}

View File

@ -0,0 +1,139 @@
/*
* 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.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<Cursor>()
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()
}
}

View File

@ -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

View File

@ -0,0 +1,138 @@
package org.kiwix.kiwixmobile.zim_manager.fileselect_view
/*
* 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/>.
*/
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<DownloadModel>()
private val file = mockk<File>()
private val files: PublishProcessor<List<File>> = PublishProcessor.create()
private val downloads: PublishProcessor<List<DownloadModel>> = 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
)
)
)
}
}

View File

@ -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")
}