package org.ebookdroid.ui.library.adapters; import org.sufficientlysecure.viewer.R; import org.ebookdroid.common.notifications.INotificationManager; import org.ebookdroid.common.settings.LibSettings; import org.ebookdroid.ui.library.IBrowserActivity; import org.ebookdroid.ui.library.views.BookshelfView; import android.database.DataSetObserver; import android.os.Parcelable; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.emdev.common.filesystem.FileSystemScanner; import org.emdev.common.filesystem.MediaManager; import org.emdev.ui.adapters.BaseViewHolder; import org.emdev.ui.tasks.AsyncTask; import org.emdev.utils.FileUtils; import org.emdev.utils.LengthUtils; import org.emdev.utils.StringUtils; import org.emdev.utils.collections.SparseArrayEx; import org.emdev.utils.collections.TLIterator; public class BooksAdapter extends PagerAdapter implements FileSystemScanner.Listener, Iterable<BookShelfAdapter> { public final static int SERVICE_SHELVES = 2; public static final int RECENT_INDEX = 0; public static final int SEARCH_INDEX = 1; private final static AtomicInteger SEQ = new AtomicInteger(SERVICE_SHELVES); final IBrowserActivity base; final AtomicBoolean inScan = new AtomicBoolean(); final SparseArrayEx<BookShelfAdapter> data = new SparseArrayEx<BookShelfAdapter>(); final TreeMap<String, BookShelfAdapter> folders = new TreeMap<String, BookShelfAdapter>(); private final RecentUpdater updater = new RecentUpdater(); private final RecentAdapter recent; private final FileSystemScanner scanner; private final List<DataSetObserver> _dsoList = new ArrayList<DataSetObserver>(); private String searchQuery; public BooksAdapter(final IBrowserActivity base, final RecentAdapter adapter) { this.base = base; this.recent = adapter; this.recent.registerDataSetObserver(updater); this.scanner = new FileSystemScanner(base.getActivity()); this.scanner.addListener(this); this.scanner.addListener(base); this.searchQuery = LibSettings.current().searchBookQuery; } public void onDestroy() { scanner.shutdown(); data.clear(); folders.clear(); } @Override public void destroyItem(final View collection, final int position, final Object view) { ((ViewPager) collection).removeView((View) view); ((View) view).destroyDrawingCache(); } @Override public void finishUpdate(final View arg0) { // TODO Auto-generated method stub } @Override public TLIterator<BookShelfAdapter> iterator() { return data.iterator(); } @Override public int getCount() { return getListCount(); } @Override public Object instantiateItem(final View arg0, final int arg1) { final BookshelfView view = new BookshelfView(base, arg0, getList(arg1)); ((ViewPager) arg0).addView(view, 0); return view; } @Override public boolean isViewFromObject(final View arg0, final Object arg1) { return arg0.equals(arg1); } @Override public void restoreState(final Parcelable arg0, final ClassLoader arg1) { // TODO Auto-generated method stub } @Override public Parcelable saveState() { return null; } @Override public void startUpdate(final View arg0) { // TODO Auto-generated method stub } private void addShelf(final BookShelfAdapter a) { data.append(a.id, a); folders.put(a.path, a); if (a.mpath != null) { folders.put(a.mpath, a); } } private void removeShelf(final BookShelfAdapter a) { data.remove(a.id); folders.remove(a.path); if (a.mpath != null) { folders.remove(a.mpath); } } public synchronized BookShelfAdapter getShelf(final String path) { final BookShelfAdapter a = folders.get(path); if (a != null) { return a; } final String mpath = FileUtils.invertMountPrefix(path); return mpath != null ? folders.get(path) : null; } public synchronized int getShelfPosition(final BookShelfAdapter shelf) { checkServiceAdapters(); return data.indexOfValue(shelf); } public synchronized BookShelfAdapter getList(final int index) { return data.valueAt(index); } public synchronized int getListCount() { return data.size(); } public synchronized int getListCount(final int currentList) { checkServiceAdapters(); if (0 <= currentList && currentList < data.size()) { return getList(currentList).nodes.size(); } return 0; } public String getListName(final int currentList) { checkServiceAdapters(); final BookShelfAdapter list = getList(currentList); return list != null ? LengthUtils.safeString(list.name) : ""; } @Override public CharSequence getPageTitle(final int position) { return getListName(position); } public String getListPath(final int currentList) { checkServiceAdapters(); final BookShelfAdapter list = getList(currentList); return list != null ? LengthUtils.safeString(list.path) : ""; } public synchronized List<String> getListNames() { checkServiceAdapters(); final int size = data.size(); if (size == 0) { return null; } final List<String> result = new ArrayList<String>(data.size()); for (int index = 0; index < size; index++) { final BookShelfAdapter a = data.valueAt(index); result.add(a.name); } return result; } public synchronized List<String> getListPaths() { checkServiceAdapters(); final int size = data.size(); if (size == 0) { return null; } final List<String> result = new ArrayList<String>(data.size()); for (int index = 0; index < size; index++) { final BookShelfAdapter a = data.valueAt(index); result.add(a.path); } return result; } public synchronized BookNode getItem(final int currentList, final int position) { checkServiceAdapters(); if (0 <= currentList && currentList < data.size()) { return getList(currentList).nodes.get(position); } throw new RuntimeException("Wrong list id: " + currentList + "/" + data.size()); } public long getItemId(final int position) { return position; } public synchronized void clearData() { getService(SEARCH_INDEX).nodes.clear(); final BookShelfAdapter[] service = new BookShelfAdapter[SERVICE_SHELVES]; for (int i = 0; i < service.length; i++) { service[i] = data.get(i); } data.clear(); folders.clear(); SEQ.set(SERVICE_SHELVES); for (int i = 0; i < service.length; i++) { if (service[i] != null) { data.append(i, service[i]); } else { getService(i); } } notifyDataSetChanged(); } public synchronized void clearSearch() { final BookShelfAdapter search = getService(SEARCH_INDEX); search.nodes.clear(); search.notifyDataSetChanged(); } protected synchronized void checkServiceAdapters() { for (int i = 0; i < SERVICE_SHELVES; i++) { getService(i); } } protected synchronized BookShelfAdapter getService(final int index) { BookShelfAdapter a = data.get(index); if (a == null) { switch (index) { case RECENT_INDEX: a = new BookShelfAdapter(base, 0, base.getContext().getString(R.string.recent_title), ""); break; case SEARCH_INDEX: a = new BookShelfAdapter(base, 0, base.getContext().getString(R.string.search_results_title), ""); break; } if (a != null) { data.append(index, a); } } return a; } public void startScan() { clearData(); final LibSettings libSettings = LibSettings.current(); final Set<String> folders = new LinkedHashSet<String>(libSettings.autoScanDirs); if (libSettings.autoScanRemovableMedia) { folders.addAll(MediaManager.getReadableMedia()); } scanner.startScan(libSettings.allowedFileTypes, folders); } public void startScan(final String path) { final LibSettings libSettings = LibSettings.current(); scanner.startScan(libSettings.allowedFileTypes, path); } public void startScan(final Collection<String> paths) { final LibSettings libSettings = LibSettings.current(); scanner.startScan(libSettings.allowedFileTypes, paths); } public void stopScan() { scanner.stopScan(); } public synchronized void removeAll(final Collection<String> paths) { boolean found = false; for (final String path : paths) { scanner.stopObservers(path); found |= removeAllImpl(path); } if (found) { notifyDataSetChanged(); } } public synchronized void removeAll(final String path) { scanner.stopObservers(path); if (removeAllImpl(path)) { notifyDataSetChanged(); } } private boolean removeAllImpl(final String path) { final String ap = path + "/"; final TLIterator<BookShelfAdapter> iter = data.iterator(); boolean found = false; while (iter.hasNext()) { final BookShelfAdapter next = iter.next(); final boolean eq = next.path.startsWith(ap) || next.path.equals(path) || next.mpath != null && (next.mpath.startsWith(ap) || next.mpath.equals(path)); if (eq) { folders.remove(next.path); if (next.mpath != null) { folders.remove(next.mpath); } iter.remove(); found = true; } } iter.release(); return found; } public boolean startSearch(final String searchQuery) { this.searchQuery = LengthUtils.safeString(searchQuery).trim(); LibSettings.updateSearchBookQuery(this.searchQuery); clearSearch(); if (LengthUtils.isEmpty(this.searchQuery)) { return false; } if (!scanner.isScan()) { new SearchTask().execute(""); } return true; } public String getSearchQuery() { return searchQuery; } protected synchronized void onNodesFound(final List<BookNode> nodes) { final BookShelfAdapter search = getService(SEARCH_INDEX); search.nodes.addAll(nodes); Collections.sort(search.nodes); search.notifyDataSetChanged(); } @Override public synchronized void onFileScan(final File parent, final File[] files) { final String dir = parent.getAbsolutePath(); BookShelfAdapter a = getShelf(dir); if (LengthUtils.isEmpty(files)) { if (a != null) { onDirDeleted(parent.getParentFile(), parent); } return; } boolean newShelf = false; if (a == null) { a = new BookShelfAdapter(base, SEQ.getAndIncrement(), parent.getName(), dir); addShelf(a); newShelf = true; } final BookShelfAdapter search = getService(SEARCH_INDEX); boolean found = false; for (final File f : files) { BookNode node = recent.getNode(f.getAbsolutePath()); if (node == null) { node = new BookNode(f, null); } a.nodes.add(node); if (acceptSearch(node)) { found = true; search.nodes.add(node); } } if (newShelf) { notifyDataSetChanged(); } else { a.notifyDataSetChanged(); } if (found) { Collections.sort(search.nodes); search.notifyDataSetChanged(); } } @Override public synchronized void onFileAdded(final File parent, final File f) { if (f == null) { return; } if (!LibSettings.current().allowedFileTypes.accept(f)) { return; } final String dir = parent.getAbsolutePath(); boolean newShelf = false; BookShelfAdapter a = getShelf(dir); if (a == null) { a = new BookShelfAdapter(base, SEQ.getAndIncrement(), parent.getName(), dir); addShelf(a); newShelf = true; } BookNode node = recent.getNode(f.getAbsolutePath()); if (node == null) { node = new BookNode(f, null); } a.nodes.add(node); Collections.sort(a.nodes); if (newShelf) { notifyDataSetChanged(); } else { a.notifyDataSetChanged(); } if (acceptSearch(node)) { final BookShelfAdapter search = getService(SEARCH_INDEX); search.nodes.add(node); Collections.sort(search.nodes); search.notifyDataSetChanged(); } if (LibSettings.current().showNotifications) { INotificationManager.instance.notify(R.string.notification_file_add, f.getAbsolutePath(), null); } } @Override public synchronized void onFileDeleted(final File parent, final File f) { if (f == null) { return; } final BookShelfAdapter a = getShelf(parent.getAbsolutePath()); if (a == null) { return; } final String path = f.getAbsolutePath(); final BookShelfAdapter search = getService(SEARCH_INDEX); for (final Iterator<BookNode> i = a.nodes.iterator(); i.hasNext();) { final BookNode node = i.next(); if (path.equals(node.path)) { i.remove(); if (a.nodes.isEmpty()) { removeShelf(a); this.notifyDataSetChanged(); } else { a.notifyDataSetChanged(); } if (search.nodes.remove(node)) { search.notifyDataSetChanged(); } if (LibSettings.current().showNotifications) { INotificationManager.instance.notify(R.string.notification_file_delete, f.getAbsolutePath(), null); } return; } } } @Override public void onDirAdded(final File parent, final File f) { final LibSettings libSettings = LibSettings.current(); scanner.startScan(libSettings.allowedFileTypes, f.getAbsolutePath()); } @Override public synchronized void onDirDeleted(final File parent, final File f) { final String dir = f.getAbsolutePath(); final BookShelfAdapter a = getShelf(dir); if (a != null) { removeShelf(a); this.notifyDataSetChanged(); } } protected boolean acceptSearch(final BookNode node) { if (LengthUtils.isEmpty(searchQuery)) { return false; } final String bookTitle = StringUtils.cleanupTitle(node.name).toLowerCase(); final int pos = bookTitle.indexOf(searchQuery); return pos >= 0; } public void registerDataSetObserver(final DataSetObserver dataSetObserver) { _dsoList.add(dataSetObserver); } protected void notifyDataSetInvalidated() { for (final DataSetObserver dso : _dsoList) { dso.onInvalidated(); } } @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); for (final DataSetObserver dso : _dsoList) { dso.onChanged(); } } public static class ViewHolder extends BaseViewHolder { ImageView imageView; TextView textView; @Override public void init(final View convertView) { super.init(convertView); this.imageView = (ImageView) convertView.findViewById(R.id.thumbnailImage); this.textView = (TextView) convertView.findViewById(R.id.thumbnailText); } } private final class RecentUpdater extends DataSetObserver { @Override public void onChanged() { updateRecentBooks(); } @Override public void onInvalidated() { updateRecentBooks(); } private void updateRecentBooks() { final BookShelfAdapter ra = getService(RECENT_INDEX); ra.nodes.clear(); final int count = recent.getCount(); for (int i = 0; i < count; i++) { final BookNode book = recent.getItem(i); ra.nodes.add(book); final BookShelfAdapter a = getShelf(new File(book.path).getParent()); if (a != null) { a.notifyDataSetInvalidated(); } } ra.notifyDataSetChanged(); BooksAdapter.this.notifyDataSetInvalidated(); } } class SearchTask extends AsyncTask<String, String, Void> { private final BlockingQueue<BookNode> queue = new ArrayBlockingQueue<BookNode>(160, true); @Override protected void onPreExecute() { base.showProgress(true); } @Override protected Void doInBackground(final String... paths) { int aIndex = SERVICE_SHELVES; while (aIndex < getListCount()) { int nIndex = 0; while (nIndex < getListCount(aIndex)) { final BookNode node = getItem(aIndex, nIndex); if (acceptSearch(node)) { queue.offer(node); publishProgress(""); } nIndex++; } aIndex++; } return null; } @Override protected void onProgressUpdate(final String... values) { final ArrayList<BookNode> nodes = new ArrayList<BookNode>(); while (!queue.isEmpty()) { nodes.add(queue.poll()); } if (!nodes.isEmpty()) { onNodesFound(nodes); } } @Override protected void onPostExecute(final Void v) { onProgressUpdate(""); base.showProgress(false); } } }