mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-22 12:03:09 -04:00
Merge remote-tracking branch 'origin/develop' into feature/macgills/#1214-nightly-and-release-uploads
This commit is contained in:
commit
292d26da73
@ -226,20 +226,14 @@ android {
|
||||
testBuildType "debug"
|
||||
|
||||
lintOptions {
|
||||
// Treat lint seriously
|
||||
abortOnError true
|
||||
// Hardcoded strings in xml
|
||||
error "HardcodedText"
|
||||
// Hardcoded strings in setText
|
||||
error "SetTextI18n"
|
||||
// Strings.xml issues
|
||||
warning "ExtraTranslation"
|
||||
warning "MissingTranslation"
|
||||
|
||||
// Warnings
|
||||
warning "InvalidPackage"
|
||||
warning "StringFormatInvalid"
|
||||
checkAllWarnings true
|
||||
warningsAsErrors true
|
||||
ignore 'MissingTranslation', //TODO stop ignoring
|
||||
'CheckResult'
|
||||
baseline file("lint-baseline.xml")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.all {
|
||||
|
16748
app/lint-baseline.xml
Normal file
16748
app/lint-baseline.xml
Normal file
File diff suppressed because it is too large
Load Diff
@ -218,7 +218,7 @@
|
||||
},
|
||||
{
|
||||
"id": "2:6862771806221961183",
|
||||
"name": "zimID"
|
||||
"name": "zimId"
|
||||
},
|
||||
{
|
||||
"id": "3:4312769031500860715",
|
||||
|
@ -23,6 +23,7 @@ import dagger.Subcomponent
|
||||
import org.kiwix.kiwixmobile.di.modules.ActivityModule
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadFragment
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.ZimFileSelectFragment
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.DeleteFiles
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.LibraryFragment
|
||||
|
||||
@Subcomponent(modules = [ActivityModule::class])
|
||||
@ -33,6 +34,8 @@ interface ActivityComponent {
|
||||
|
||||
fun inject(zimFileSelectFragment: ZimFileSelectFragment)
|
||||
|
||||
fun inject(deleteFiles: DeleteFiles)
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
package org.kiwix.kiwixmobile.extensions
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.ActionMode
|
||||
import android.view.ActionMode.Callback
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
|
||||
fun Activity.startActionMode(
|
||||
menuId: Int,
|
||||
idsToClickActions: Map<Int, () -> Any>,
|
||||
onDestroyAction: () -> Unit
|
||||
): ActionMode? {
|
||||
return startActionMode(object : Callback {
|
||||
override fun onActionItemClicked(
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
) = idsToClickActions[item.itemId]?.let {
|
||||
it()
|
||||
mode.finish()
|
||||
true
|
||||
} ?: false
|
||||
|
||||
override fun onCreateActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu?
|
||||
): Boolean {
|
||||
mode.getMenuInflater()
|
||||
.inflate(menuId, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
mode: ActionMode?,
|
||||
menu: Menu?
|
||||
) = false
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
onDestroyAction()
|
||||
}
|
||||
|
||||
})
|
||||
}
|
@ -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);
|
||||
@ -377,6 +364,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
|
||||
open(bookOnDiskItem);
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
null,
|
||||
null),
|
||||
BookOnDiskDelegate.LanguageDelegate.INSTANCE
|
||||
);
|
||||
@ -739,7 +727,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 +1186,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 +1215,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
|
||||
@ -2105,7 +2099,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
|
||||
|
||||
@Override
|
||||
public void addBooks(List<BooksOnDiskListItem> books) {
|
||||
booksAdapter.setItemList(books);
|
||||
booksAdapter.setItems(books);
|
||||
}
|
||||
|
||||
private void searchFiles() {
|
||||
@ -2116,7 +2110,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 ->
|
||||
filesSystemFiles + 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 ->
|
||||
acc.apply { addAll(scanDirectory(root)) }
|
||||
})
|
||||
|
||||
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 { _, name -> name.endsWithAny(*zimFileExtensions) }
|
||||
?.toList()
|
||||
|
||||
}
|
||||
|
||||
internal fun String.endsWithAny(vararg suffixes: String) =
|
||||
suffixes.fold(false, { acc, s -> acc or endsWith(s) })
|
@ -52,9 +52,24 @@ import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4G
|
||||
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.ZimManageViewModel.FileSelectActions.MultiModeFinished
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestDeleteMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestOpen
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestSelect
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestShareMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.FileSelectListState
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode.MULTI
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode.NORMAL
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.DeleteFiles
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.None
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.OpenFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.ShareFiles
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.SideEffect
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.StartMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem.BookItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem.DividerItem
|
||||
@ -79,16 +94,26 @@ class ZimManageViewModel @Inject constructor(
|
||||
private val defaultLanguageProvider: DefaultLanguageProvider,
|
||||
private val dataSource: DataSource
|
||||
) : ViewModel() {
|
||||
sealed class FileSelectActions {
|
||||
data class RequestOpen(val bookOnDisk: BookOnDisk) : FileSelectActions()
|
||||
data class RequestSelect(val bookOnDisk: BookOnDisk) : FileSelectActions()
|
||||
data class RequestMultiSelection(val bookOnDisk: BookOnDisk) : FileSelectActions()
|
||||
object RequestDeleteMultiSelection : FileSelectActions()
|
||||
object RequestShareMultiSelection : FileSelectActions()
|
||||
object MultiModeFinished : FileSelectActions()
|
||||
}
|
||||
|
||||
val sideEffects = PublishProcessor.create<SideEffect<out Any?>>()
|
||||
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
|
||||
val downloadItems: MutableLiveData<List<DownloadItem>> = MutableLiveData()
|
||||
val bookItems: MutableLiveData<List<BooksOnDiskListItem>> = MutableLiveData()
|
||||
val fileSelectListStates: MutableLiveData<FileSelectListState> = MutableLiveData()
|
||||
val deviceListIsRefreshing = MutableLiveData<Boolean>()
|
||||
val libraryListIsRefreshing = MutableLiveData<Boolean>()
|
||||
val networkStates = MutableLiveData<NetworkState>()
|
||||
val languageItems = MutableLiveData<List<Language>>()
|
||||
|
||||
val requestFileSystemCheck = PublishProcessor.create<Unit>()
|
||||
val fileSelectActions = PublishProcessor.create<FileSelectActions>()
|
||||
val requestDownloadLibrary = BehaviorProcessor.createDefault<Unit>(Unit)
|
||||
val requestFiltering = BehaviorProcessor.createDefault<String>("")
|
||||
val requestLanguagesDialog = PublishProcessor.create<Unit>()
|
||||
@ -127,10 +152,74 @@ class ZimManageViewModel @Inject constructor(
|
||||
updateLanguagesInDao(networkLibrary, languages),
|
||||
updateNetworkStates(),
|
||||
updateLanguageItemsForDialog(languages),
|
||||
requestsAndConnectivtyChangesToLibraryRequests(networkLibrary)
|
||||
requestsAndConnectivtyChangesToLibraryRequests(networkLibrary),
|
||||
fileSelectActions()
|
||||
)
|
||||
}
|
||||
|
||||
private fun fileSelectActions() = fileSelectActions.subscribe({
|
||||
sideEffects.offer(
|
||||
when (it) {
|
||||
is RequestOpen -> OpenFile(it.bookOnDisk)
|
||||
is RequestMultiSelection -> startMultiSelectionAndSelectBook(it.bookOnDisk)
|
||||
RequestDeleteMultiSelection -> DeleteFiles(selectionsFromState())
|
||||
RequestShareMultiSelection -> ShareFiles(selectionsFromState())
|
||||
MultiModeFinished -> noSideEffectAndClearSelectionState()
|
||||
is RequestSelect -> noSideEffectSelectBook(it.bookOnDisk)
|
||||
}
|
||||
)
|
||||
}, Throwable::printStackTrace)
|
||||
|
||||
private fun startMultiSelectionAndSelectBook(
|
||||
bookOnDisk: BookOnDisk
|
||||
): StartMultiSelection {
|
||||
fileSelectListStates.value?.let {
|
||||
fileSelectListStates.postValue(
|
||||
it.copy(
|
||||
bookOnDiskListItems = selectBook(it, bookOnDisk),
|
||||
selectionMode = MULTI
|
||||
)
|
||||
)
|
||||
}
|
||||
return StartMultiSelection(bookOnDisk, fileSelectActions)
|
||||
}
|
||||
|
||||
private fun selectBook(
|
||||
it: FileSelectListState,
|
||||
bookOnDisk: BookOnDisk
|
||||
): List<BooksOnDiskListItem> {
|
||||
return it.bookOnDiskListItems.map { listItem ->
|
||||
if (listItem.id == bookOnDisk.id) listItem.apply { isSelected = !isSelected }
|
||||
else listItem
|
||||
}
|
||||
}
|
||||
|
||||
private fun noSideEffectSelectBook(bookOnDisk: BookOnDisk): SideEffect<Unit> {
|
||||
fileSelectListStates.value?.let {
|
||||
fileSelectListStates.postValue(
|
||||
it.copy(bookOnDiskListItems = it.bookOnDiskListItems.map { listItem ->
|
||||
if (listItem.id == bookOnDisk.id) listItem.apply { isSelected = !isSelected }
|
||||
else listItem
|
||||
})
|
||||
)
|
||||
}
|
||||
return None
|
||||
}
|
||||
|
||||
private fun selectionsFromState() = fileSelectListStates.value?.selectedBooks ?: emptyList()
|
||||
|
||||
private fun noSideEffectAndClearSelectionState(): SideEffect<Unit> {
|
||||
fileSelectListStates.value?.let {
|
||||
fileSelectListStates.postValue(
|
||||
it.copy(
|
||||
bookOnDiskListItems = it.bookOnDiskListItems.map { it.apply { isSelected = false } },
|
||||
selectionMode = NORMAL
|
||||
)
|
||||
)
|
||||
}
|
||||
return None
|
||||
}
|
||||
|
||||
private fun requestsAndConnectivtyChangesToLibraryRequests(library: PublishProcessor<LibraryNetworkEntity>) =
|
||||
Flowable.combineLatest(
|
||||
requestDownloadLibrary,
|
||||
@ -411,10 +500,27 @@ class ZimManageViewModel @Inject constructor(
|
||||
private fun updateBookItems() =
|
||||
dataSource.booksOnDiskAsListItems()
|
||||
.subscribe(
|
||||
bookItems::postValue,
|
||||
{ newList ->
|
||||
fileSelectListStates.postValue(
|
||||
fileSelectListStates.value?.let { inheritSelections(it, newList) }
|
||||
?: FileSelectListState(newList)
|
||||
)
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun inheritSelections(
|
||||
oldState: FileSelectListState,
|
||||
newList: MutableList<BooksOnDiskListItem>
|
||||
): FileSelectListState {
|
||||
return oldState.copy(
|
||||
bookOnDiskListItems = newList.map { newBookOnDisk ->
|
||||
val firstOrNull =
|
||||
oldState.bookOnDiskListItems.firstOrNull { oldBookOnDisk -> oldBookOnDisk.id == newBookOnDisk.id }
|
||||
newBookOnDisk.apply { isSelected = firstOrNull?.isSelected ?: false }
|
||||
})
|
||||
}
|
||||
|
||||
private fun removeCompletedDownloadsFromDb(downloadStatuses: Flowable<List<DownloadStatus>>) =
|
||||
downloadStatuses
|
||||
.observeOn(Schedulers.io())
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.fileselect_view
|
||||
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode.NORMAL
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
|
||||
data class FileSelectListState(
|
||||
val bookOnDiskListItems: List<BooksOnDiskListItem>,
|
||||
val selectionMode: SelectionMode = NORMAL
|
||||
) {
|
||||
val selectedBooks by lazy {
|
||||
bookOnDiskListItems.filter { it.isSelected }.filterIsInstance(BookOnDisk::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class SelectionMode {
|
||||
NORMAL,
|
||||
MULTI
|
||||
}
|
@ -1,45 +1,80 @@
|
||||
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
|
||||
|
||||
override fun onScanCompleted() {
|
||||
_booksOnFileSystem.onNext(foundBooks.toList())
|
||||
private fun scanFiles() = fileSearch.scan(sharedPreferenceUtil.prefStorage)
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
}
|
||||
}).scan(sharedPreferenceUtil.prefStorage)
|
||||
private fun convertToBookOnDisk(file: File): BookOnDisk? {
|
||||
configureZimContentProvider()
|
||||
var bookOnDisk: BookOnDisk? = null
|
||||
if (ZimContentProvider.canIterate && ZimContentProvider.setZimFile(file.absolutePath) != null) {
|
||||
bookOnDisk = BookOnDisk(book = bookFromZimContentProvider(), file = file)
|
||||
}
|
||||
resetZimContentProvider()
|
||||
return bookOnDisk
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private fun configureZimContentProvider() {
|
||||
if (ZimContentProvider.zimFileName != null) {
|
||||
ZimContentProvider.originalFileName = ZimContentProvider.zimFileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetZimContentProvider() {
|
||||
if (ZimContentProvider.originalFileName != "") {
|
||||
ZimContentProvider.setZimFile(ZimContentProvider.originalFileName)
|
||||
}
|
||||
ZimContentProvider.originalFileName = ""
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ package org.kiwix.kiwixmobile.zim_manager.fileselect_view
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.ActionMode
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -32,49 +32,49 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.zim_list.file_management_no_files
|
||||
import kotlinx.android.synthetic.main.zim_list.zim_swiperefresh
|
||||
import kotlinx.android.synthetic.main.zim_list.zimfilelist
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.R.string
|
||||
import org.kiwix.kiwixmobile.base.BaseFragment
|
||||
import org.kiwix.kiwixmobile.data.ZimContentProvider
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
|
||||
import org.kiwix.kiwixmobile.di.components.ActivityComponent
|
||||
import org.kiwix.kiwixmobile.extensions.toast
|
||||
import org.kiwix.kiwixmobile.utils.BookUtils
|
||||
import org.kiwix.kiwixmobile.utils.Constants.REQUEST_STORAGE_PERMISSION
|
||||
import org.kiwix.kiwixmobile.utils.DialogShower
|
||||
import org.kiwix.kiwixmobile.utils.KiwixDialog.DeleteZim
|
||||
import org.kiwix.kiwixmobile.utils.LanguageUtils
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.utils.files.FileUtils
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestOpen
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestSelect
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate.BookDelegate
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskDelegate.LanguageDelegate
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskAdapter
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import javax.inject.Inject
|
||||
|
||||
class ZimFileSelectFragment : BaseFragment() {
|
||||
|
||||
@Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil
|
||||
@Inject lateinit var bookDao: NewBookDao
|
||||
@Inject lateinit var dialogShower: DialogShower
|
||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
@Inject lateinit var bookUtils: BookUtils
|
||||
|
||||
private var actionMode: ActionMode? = null
|
||||
val disposable = CompositeDisposable()
|
||||
|
||||
private val zimManageViewModel: ZimManageViewModel by lazy {
|
||||
ViewModelProviders.of(activity!!, viewModelFactory)
|
||||
.get(ZimManageViewModel::class.java)
|
||||
}
|
||||
|
||||
private val bookDelegate: BookDelegate by lazy {
|
||||
BookDelegate(sharedPreferenceUtil,
|
||||
{ offerAction(RequestOpen(it)) },
|
||||
{ offerAction(RequestMultiSelection(it)) },
|
||||
{ offerAction(RequestSelect(it)) })
|
||||
}
|
||||
|
||||
private val booksOnDiskAdapter: BooksOnDiskAdapter by lazy {
|
||||
BooksOnDiskAdapter(
|
||||
BookDelegate(sharedPreferenceUtil, this::openOnClick, this::deleteOnLongClick),
|
||||
LanguageDelegate
|
||||
)
|
||||
BooksOnDiskAdapter(bookDelegate, LanguageDelegate)
|
||||
}
|
||||
|
||||
override fun inject(activityComponent: ActivityComponent) {
|
||||
@ -101,31 +101,51 @@ class ZimFileSelectFragment : BaseFragment() {
|
||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
zimManageViewModel.bookItems.observe(this, Observer {
|
||||
booksOnDiskAdapter.itemList = it!!
|
||||
checkEmpty(it)
|
||||
})
|
||||
zimManageViewModel.fileSelectListStates.observe(this, Observer { render(it) })
|
||||
disposable.add(sideEffects())
|
||||
zimManageViewModel.deviceListIsRefreshing.observe(this, Observer {
|
||||
zim_swiperefresh.isRefreshing = it!!
|
||||
})
|
||||
}
|
||||
|
||||
private fun sideEffects() = zimManageViewModel.sideEffects.subscribe(
|
||||
{
|
||||
val effectResult = it.invokeWith(activity!!)
|
||||
if (effectResult is ActionMode) {
|
||||
actionMode = effectResult
|
||||
}
|
||||
}, Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun render(state: FileSelectListState) {
|
||||
val items = state.bookOnDiskListItems
|
||||
bookDelegate.selectionMode = state.selectionMode
|
||||
booksOnDiskAdapter.items = items
|
||||
actionMode?.title = String.format("%d", state.selectedBooks.size)
|
||||
file_management_no_files.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
private fun checkEmpty(books: List<Any>) {
|
||||
file_management_no_files.visibility =
|
||||
if (books.isEmpty()) View.VISIBLE
|
||||
else View.GONE
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
private fun offerAction(
|
||||
action: FileSelectActions
|
||||
) {
|
||||
zimManageViewModel.fileSelectActions.offer(action)
|
||||
}
|
||||
|
||||
private fun checkPermissions() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
activity!!,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT > 18
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
context.toast(R.string.request_storage)
|
||||
requestPermissions(
|
||||
@ -140,34 +160,4 @@ class ZimFileSelectFragment : BaseFragment() {
|
||||
private fun requestFileSystemCheck() {
|
||||
zimManageViewModel.requestFileSystemCheck.onNext(Unit)
|
||||
}
|
||||
|
||||
private fun openOnClick(it: BookOnDisk) {
|
||||
val file = it.file
|
||||
ZimContentProvider.canIterate = false
|
||||
if (!file.canRead()) {
|
||||
context.toast(string.error_filenotfound)
|
||||
} else {
|
||||
(activity as ZimManageActivity).finishResult(file.path)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteOnLongClick(it: BookOnDisk) {
|
||||
dialogShower.show(DeleteZim, {
|
||||
if (deleteSpecificZimFile(it)) {
|
||||
context.toast(string.delete_specific_zim_toast)
|
||||
} else {
|
||||
context.toast(string.delete_zim_failed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun deleteSpecificZimFile(book: BookOnDisk): Boolean {
|
||||
val file = book.file
|
||||
FileUtils.deleteZimFile(file.path)
|
||||
if (file.exists()) {
|
||||
return false
|
||||
}
|
||||
bookDao.delete(book.databaseId!!)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,13 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.extensions.inflate
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode.NORMAL
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskViewHolder.BookViewHolder
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BookOnDiskViewHolder.LanguageItemViewHolder
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.LanguageItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.AbsDelegateAdapter
|
||||
@ -33,17 +35,28 @@ sealed class BookOnDiskDelegate<I : BooksOnDiskListItem, VH : BookOnDiskViewHold
|
||||
class BookDelegate(
|
||||
val sharedPreferenceUtil: SharedPreferenceUtil,
|
||||
val clickAction: (BookOnDisk) -> Unit,
|
||||
val longClickAction: ((BookOnDisk) -> Unit)? = null
|
||||
val longClickAction: ((BookOnDisk) -> Unit)? = null,
|
||||
val multiSelectAction: ((BookOnDisk) -> Unit)? = null
|
||||
) : BookOnDiskDelegate<BookOnDisk, BookViewHolder>() {
|
||||
|
||||
override val itemClass = BookOnDisk::class.java
|
||||
|
||||
var selectionMode: SelectionMode = NORMAL
|
||||
|
||||
override fun bind(
|
||||
viewHolder: ViewHolder,
|
||||
itemToBind: BooksOnDiskListItem
|
||||
) {
|
||||
(viewHolder as BookOnDiskViewHolder.BookViewHolder).bind((itemToBind as BookOnDisk), selectionMode)
|
||||
}
|
||||
|
||||
override fun createViewHolder(parent: ViewGroup) =
|
||||
BookViewHolder(
|
||||
parent.inflate(R.layout.item_book, false),
|
||||
sharedPreferenceUtil,
|
||||
clickAction,
|
||||
longClickAction
|
||||
longClickAction,
|
||||
multiSelectAction
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter
|
||||
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.AdapterDelegate
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.AdapterDelegateManager
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.BaseDelegateAdapter
|
||||
|
||||
class BooksOnDiskAdapter(
|
||||
|
@ -6,6 +6,7 @@ import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
sealed class BooksOnDiskListItem {
|
||||
var isSelected: Boolean = false
|
||||
abstract val id: Long
|
||||
|
||||
data class LanguageItem constructor(
|
||||
|
@ -3,6 +3,7 @@ package org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.header_language.header_language
|
||||
import kotlinx.android.synthetic.main.item_book.itemBookCheckbox
|
||||
import kotlinx.android.synthetic.main.item_book.item_book_article_count
|
||||
import kotlinx.android.synthetic.main.item_book.item_book_date
|
||||
import kotlinx.android.synthetic.main.item_book.item_book_description
|
||||
@ -17,6 +18,9 @@ import org.kiwix.kiwixmobile.main.KiwixWebView
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.zim_manager.KiloByte
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.ArticleCount
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode.MULTI
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.SelectionMode.NORMAL
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.LanguageItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.BaseViewHolder
|
||||
@ -28,10 +32,17 @@ sealed class BookOnDiskViewHolder<T : BooksOnDiskListItem>(containerView: View)
|
||||
containerView: View,
|
||||
private val sharedPreferenceUtil: SharedPreferenceUtil,
|
||||
private val clickAction: (BookOnDisk) -> Unit,
|
||||
private val longClickAction: ((BookOnDisk) -> Unit)?
|
||||
private val longClickAction: ((BookOnDisk) -> Unit)?,
|
||||
private val multiSelectAction: ((BookOnDisk) -> Unit)?
|
||||
) : BookOnDiskViewHolder<BookOnDisk>(containerView) {
|
||||
|
||||
override fun bind(item: BookOnDisk) {
|
||||
}
|
||||
|
||||
fun bind(
|
||||
item: BookOnDisk,
|
||||
selectionMode: SelectionMode
|
||||
) {
|
||||
val book = item.book
|
||||
item_book_title.text = book.getTitle()
|
||||
item_book_date.text = book.getDate()
|
||||
@ -59,24 +70,30 @@ sealed class BookOnDiskViewHolder<T : BooksOnDiskListItem>(containerView: View)
|
||||
item_book_label_video.visibility = View.GONE
|
||||
}
|
||||
|
||||
containerView.setOnClickListener {
|
||||
clickAction.invoke(item)
|
||||
}
|
||||
containerView.setOnLongClickListener {
|
||||
longClickAction?.invoke(item)
|
||||
return@setOnLongClickListener true
|
||||
itemBookCheckbox.isChecked = item.isSelected
|
||||
when (selectionMode) {
|
||||
MULTI -> {
|
||||
itemBookCheckbox.visibility = View.VISIBLE
|
||||
containerView.setOnClickListener { multiSelectAction?.invoke(item) }
|
||||
containerView.setOnLongClickListener(null)
|
||||
}
|
||||
NORMAL -> {
|
||||
itemBookCheckbox.visibility = View.GONE
|
||||
containerView.setOnClickListener { clickAction.invoke(item) }
|
||||
containerView.setOnLongClickListener {
|
||||
longClickAction?.invoke(item)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItemViewHolder(containerView: View) :
|
||||
BookOnDiskViewHolder<LanguageItem>(containerView) {
|
||||
|
||||
override fun bind(item: LanguageItem) {
|
||||
header_language.text = item.text
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LanguageItemViewHolder(containerView: View) :
|
||||
BookOnDiskViewHolder<LanguageItem>(containerView) {
|
||||
|
||||
override fun bind(item: LanguageItem) {
|
||||
header_language.text = item.text
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
|
||||
|
||||
import android.app.Activity
|
||||
import org.kiwix.kiwixmobile.R.string
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
|
||||
import org.kiwix.kiwixmobile.extensions.toast
|
||||
import org.kiwix.kiwixmobile.utils.DialogShower
|
||||
import org.kiwix.kiwixmobile.utils.KiwixDialog.DeleteZim
|
||||
import org.kiwix.kiwixmobile.utils.files.FileUtils
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteFiles(val booksOnDiskListItem: List<BookOnDisk>) :
|
||||
SideEffect<Unit> {
|
||||
|
||||
@Inject lateinit var dialogShower: DialogShower
|
||||
@Inject lateinit var newBookDao: NewBookDao
|
||||
|
||||
override fun invokeWith(activity: Activity) {
|
||||
activityComponent(activity).inject(this)
|
||||
booksOnDiskListItem.forEach {
|
||||
dialogShower.show(DeleteZim, {
|
||||
if (deleteSpecificZimFile(it)) {
|
||||
activity.toast(string.delete_specific_zim_toast)
|
||||
} else {
|
||||
activity.toast(string.delete_zim_failed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteSpecificZimFile(book: BookOnDisk): Boolean {
|
||||
val file = book.file
|
||||
FileUtils.deleteZimFile(file.path)
|
||||
if (file.exists()) {
|
||||
return false
|
||||
}
|
||||
newBookDao.delete(book.databaseId!!)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
object None : SideEffect<Unit> {
|
||||
override fun invokeWith(activity: Activity) {
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.fileselect_view.effects
|
||||
|
||||
import android.app.Activity
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.data.ZimContentProvider
|
||||
import org.kiwix.kiwixmobile.extensions.toast
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
|
||||
class OpenFile(val bookOnDisk: BookOnDisk): SideEffect<Unit> {
|
||||
|
||||
override fun invokeWith(activity: Activity) {
|
||||
val file = bookOnDisk.file
|
||||
ZimContentProvider.canIterate = false
|
||||
if (!file.canRead()) {
|
||||
activity.toast(R.string.error_filenotfound)
|
||||
} else {
|
||||
(activity as ZimManageActivity).finishResult(file.path)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.FileProvider
|
||||
import org.kiwix.kiwixmobile.BuildConfig
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
|
||||
class ShareFiles(val selectedBooks: List<BookOnDisk>) : SideEffect<Unit> {
|
||||
override fun invokeWith(activity: Activity) {
|
||||
val selectedFileShareIntent = Intent()
|
||||
selectedFileShareIntent.action = Intent.ACTION_SEND_MULTIPLE
|
||||
selectedFileShareIntent.type = "application/octet-stream"
|
||||
val selectedFileContentURIs = selectedBooks.mapNotNull {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
FileProvider.getUriForFile(
|
||||
activity,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
it.file
|
||||
)
|
||||
} else {
|
||||
Uri.fromFile(it.file)
|
||||
}
|
||||
}
|
||||
selectedFileShareIntent.putParcelableArrayListExtra(
|
||||
Intent.EXTRA_STREAM,
|
||||
ArrayList(selectedFileContentURIs)
|
||||
)
|
||||
selectedFileShareIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
val shareChooserIntent = Intent.createChooser(
|
||||
selectedFileShareIntent,
|
||||
activity.getString(R.string.selected_file_cab_app_chooser_title)
|
||||
)
|
||||
if (shareChooserIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
activity.startActivity(shareChooserIntent) // Open the app chooser dialog
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.fileselect_view.effects
|
||||
|
||||
import android.app.Activity
|
||||
import org.kiwix.kiwixmobile.KiwixApplication
|
||||
|
||||
interface SideEffect<T:Any?> {
|
||||
fun invokeWith(activity: Activity):T
|
||||
fun activityComponent(activity: Activity) =
|
||||
KiwixApplication.getApplicationComponent().activityComponent().activity(activity).build()
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.ActionMode
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.extensions.startActionMode
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestDeleteMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestShareMultiSelection
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
|
||||
|
||||
data class StartMultiSelection(
|
||||
val bookOnDisk: BooksOnDiskListItem.BookOnDisk,
|
||||
val fileSelectActions: PublishProcessor<FileSelectActions>
|
||||
) : SideEffect<ActionMode?> {
|
||||
override fun invokeWith(activity: Activity) =
|
||||
activity.startActionMode(
|
||||
R.menu.menu_zim_files_contextual,
|
||||
mapOf(
|
||||
R.id.zim_file_delete_item to { fileSelectActions.offer(RequestDeleteMultiSelection) },
|
||||
R.id.zim_file_share_item to { fileSelectActions.offer(RequestShareMultiSelection) }
|
||||
),
|
||||
{ fileSelectActions.offer(FileSelectActions.MultiModeFinished) }
|
||||
)
|
||||
|
||||
}
|
@ -140,7 +140,7 @@ class LibraryFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private fun onLibraryItemsChange(it: List<LibraryListItem>?) {
|
||||
libraryAdapter.itemList = it!!
|
||||
libraryAdapter.items = it!!
|
||||
if (it.isEmpty()) {
|
||||
libraryErrorText.setText(
|
||||
if (isNotConnected) R.string.no_network_connection
|
||||
|
@ -30,7 +30,7 @@ abstract class BaseDelegateAdapter<ITEM>(
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
var itemList: List<ITEM> = mutableListOf()
|
||||
var items: List<ITEM> = mutableListOf()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
@ -41,19 +41,19 @@ abstract class BaseDelegateAdapter<ITEM>(
|
||||
viewType: Int
|
||||
) = delegateManager.createViewHolder(parent, viewType)
|
||||
|
||||
override fun getItemCount() = itemList.size
|
||||
override fun getItemCount() = items.size
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
delegateManager.onBindViewHolder(itemList[position], holder)
|
||||
delegateManager.onBindViewHolder(items[position], holder)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
delegateManager.getViewTypeFor(itemList[position])
|
||||
delegateManager.getViewTypeFor(items[position])
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getIdFor(itemList[position])
|
||||
return getIdFor(items[position])
|
||||
}
|
||||
|
||||
abstract fun getIdFor(item:ITEM):Long
|
||||
|
@ -4,21 +4,35 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingRight="0dp"
|
||||
android:paddingStart="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:ignore="Overdraw"
|
||||
>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/itemBookCheckbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="RtlSymmetry"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_book_icon"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/favicon"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/itemBookCheckbox"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@mipmap/kiwix_icon_round"
|
||||
/>
|
||||
|
@ -1 +0,0 @@
|
||||
values-id
|
@ -1 +0,0 @@
|
||||
values-he
|
@ -1 +0,0 @@
|
||||
values-yi
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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,12 +51,15 @@ 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
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
|
||||
import org.kiwix.kiwixmobile.zim_manager.NetworkState.NOT_CONNECTED
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.FileSelectListState
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
@ -104,26 +103,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
|
||||
@ -235,8 +222,8 @@ class ZimManageViewModelTest {
|
||||
val expectedList = listOf(bookOnDisk())
|
||||
booksOnDiskListItems.onNext(expectedList)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.bookItems.test()
|
||||
.assertValue(expectedList)
|
||||
viewModel.fileSelectListStates.test()
|
||||
.assertValue(FileSelectListState(expectedList))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,149 @@
|
||||
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.mockk.verify
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
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.library.entity.LibraryNetworkEntity.Book
|
||||
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`() {
|
||||
withFiltering()
|
||||
booksOnFileSystem().assertValues(listOf())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null books from ZimContentProvider are filtered out`() {
|
||||
withNoFiltering()
|
||||
booksOnFileSystem().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)
|
||||
withNoFiltering()
|
||||
every { ZimContentProvider.setZimFile("This won't match") } returns ""
|
||||
expect(expectedBook)
|
||||
booksOnFileSystem().assertValues(
|
||||
listOf(
|
||||
bookOnDisk(book = expectedBook, file = file, databaseId = null)
|
||||
)
|
||||
)
|
||||
assertThat(ZimContentProvider.originalFileName).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zim provider sets zim file to original file name if it exists`() {
|
||||
withNoFiltering()
|
||||
mockkStatic(ZimContentProvider::class)
|
||||
every { ZimContentProvider.setZimFile(any()) } returns null
|
||||
ZimContentProvider.zimFileName = "myZimFileName"
|
||||
booksOnFileSystem().assertValues(listOf())
|
||||
verify { ZimContentProvider.setZimFile("myZimFileName") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zim provider does not read book if it can not iterate`() {
|
||||
withNoFiltering()
|
||||
ZimContentProvider.canIterate = false
|
||||
booksOnFileSystem().assertValues(listOf())
|
||||
}
|
||||
|
||||
private fun booksOnFileSystem() = storageObserver.booksOnFileSystem
|
||||
.test()
|
||||
.also {
|
||||
downloads.offer(listOf(downloadModel))
|
||||
files.offer(listOf(file))
|
||||
}
|
||||
|
||||
private fun expect(expectedBook: Book) {
|
||||
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
|
||||
}
|
||||
|
||||
private fun withFiltering() {
|
||||
every { downloadModel.fileNameFromUrl } returns "test"
|
||||
every { file.absolutePath } returns "This is a test"
|
||||
}
|
||||
|
||||
private fun withNoFiltering() {
|
||||
every { downloadModel.fileNameFromUrl } returns "test"
|
||||
every { file.absolutePath } returns "This won't match"
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user