Download search indexes alongside the book

This commit is contained in:
Bradlee Speice 2015-01-13 22:41:38 -05:00
parent 7ffc59d66c
commit 902b91776d
6 changed files with 125 additions and 77 deletions

View File

@ -2,7 +2,6 @@ package org.bspeice.minimalbible.test.activity.downloader.manager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import org.bspeice.minimalbible.Injector;
import org.bspeice.minimalbible.activity.downloader.DownloadPrefs;
@ -40,6 +39,7 @@ import dagger.Provides;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.subjects.PublishSubject;
import static com.jayway.awaitility.Awaitility.await;
import static org.junit.Assert.assertFalse;
@ -82,38 +82,42 @@ public class BookManagerTest implements Injector {
});
}
@Ignore
// TODO: Why doesn't this work?
@Ignore("Should be working, but isn't...")
@Test
public void testInstallBook() throws Exception {
final Book toInstall = installableBooks().toBlocking().first();
bookManager.installBook(toInstall);
final AtomicBoolean signal = new AtomicBoolean(false);
bookManager.getDownloadEvents()
.subscribe(new Action1<DLProgressEvent>() {
@Override
public void call(DLProgressEvent dlProgressEvent) {
System.out.println(dlProgressEvent.getAverageProgress());
if (dlProgressEvent.getB().getInitials().equals(toInstall.getInitials())
&& dlProgressEvent.getProgress() == DLProgressEvent.PROGRESS_COMPLETE) {
&& dlProgressEvent.getAverageProgress() == DLProgressEvent.PROGRESS_COMPLETE) {
signal.set(true);
}
}
});
await().atMost(60, TimeUnit.SECONDS)
bookManager.downloadBook(toInstall);
await().atMost(30, TimeUnit.SECONDS)
.untilTrue(signal);
}
@Ignore
// TODO: Why doesn't this work?
@Ignore("Should be working, but isn't...")
@Test
public void testJobIdMatch() {
final Book toInstall = installableBooks().toBlocking().first();
final String jobName = bookManager.getJobId(toInstall);
final String jobName = bookManager.getJobNames(toInstall).get(0);
final AtomicBoolean jobNameMatch = new AtomicBoolean(false);
JobManager.addWorkListener(new WorkListener() {
@Override
public void workProgressed(WorkEvent ev) {
Log.d("testJobIdMatch", ev.getJob().getJobID() + " " + jobName);
if (ev.getJob().getJobID().equals(jobName)) {
jobNameMatch.set(true);
}
@ -124,8 +128,8 @@ public class BookManagerTest implements Injector {
}
});
bookManager.installBook(toInstall);
await().atMost(5, TimeUnit.SECONDS)
bookManager.downloadBook(toInstall);
await().atMost(10, TimeUnit.SECONDS)
.untilTrue(jobNameMatch);
}
@ -169,20 +173,19 @@ public class BookManagerTest implements Injector {
public void testWorkProgressedCorrectProgress() {
Book mockBook = mock(Book.class);
when(mockBook.getInitials()).thenReturn("mockBook");
String bookJobName = bookManager.getJobId(mockBook);
bookManager.getBookMappings().put(bookJobName, mockBook);
String bookJobName = bookManager.getJobNames(mockBook).get(0);
bookManager.getInProgressJobNames().put(bookJobName, mockBook);
// Percent to degrees
int workProgress = 1;
int totalWork = 2;
final int circularProgress = 180;
final int workDone = 50; // 50%
// There are two jobs, each comprising 180 degrees.
// Since we are simulating one job being 50% complete, that's 90 degrees
final int circularProgress = 90;
WorkEvent ev = mock(WorkEvent.class);
Progress p = mock(Progress.class);
when(p.getJobID()).thenReturn(bookJobName);
when(p.getWorkDone()).thenReturn(workProgress);
when(p.getTotalWork()).thenReturn(totalWork);
when(p.getWork()).thenReturn(workDone);
when(ev.getJob()).thenReturn(p);
final AtomicBoolean progressCorrect = new AtomicBoolean(false);
@ -259,8 +262,15 @@ public class BookManagerTest implements Injector {
@Provides
@Singleton
BookManager bookDownloadManager(Books installed, RefreshManager rm) {
return new BookManager(installed, rm);
PublishSubject<DLProgressEvent> dlProgressEventPublisher() {
return PublishSubject.create();
}
@Provides
@Singleton
BookManager bookDownloadManager(Books installed, RefreshManager rm,
PublishSubject<DLProgressEvent> eventPublisher) {
return new BookManager(installed, rm, eventPublisher);
}
}
}

View File

@ -12,7 +12,7 @@ class DLProgressEventSpek : Spek() {{
given("a DLProgressEvent created with 50% progress and a mock book") {
val mockBook = mock(javaClass<Book>())
val dlEvent = DLProgressEvent(50, mockBook)
val dlEvent = DLProgressEvent(50, 50, mockBook)
on("getting the progress in degrees") {
val progressDegrees = dlEvent.toCircular()

View File

@ -25,6 +25,7 @@ import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.subjects.PublishSubject;
/**
* Created by bspeice on 5/20/14.
@ -47,6 +48,9 @@ public class BookItemHolder {
BookManager bookManager;
@Inject @Named("DownloadActivityContext")
Context ctx;
@Inject
PublishSubject<DLProgressEvent> downloadProgressEvents;
private Subscription subscription;
// TODO: Factory style?
@ -61,12 +65,13 @@ public class BookItemHolder {
itemName.setText(b.getName());
DLProgressEvent dlProgressEvent = bookManager.getDownloadProgress(b);
if (dlProgressEvent != null) {
displayProgress((int) dlProgressEvent.toCircular());
displayProgress(dlProgressEvent.toCircular());
} else if (bookManager.isInstalled(b)) {
displayInstalled();
}
//TODO: Refactor
subscription = bookManager.getDownloadEvents()
subscription = downloadProgressEvents
.observeOn(AndroidSchedulers.mainThread())
.filter(new Func1<DLProgressEvent, Boolean>() {
@Override
@ -98,7 +103,7 @@ public class BookItemHolder {
, Toast.LENGTH_SHORT).show();
}
} else {
bookManager.installBook(this.b);
bookManager.downloadBook(this.b);
}
}

View File

@ -6,6 +6,7 @@ import android.net.ConnectivityManager;
import org.bspeice.minimalbible.Injector;
import org.bspeice.minimalbible.MinimalBibleModules;
import org.bspeice.minimalbible.activity.downloader.manager.BookManager;
import org.bspeice.minimalbible.activity.downloader.manager.DLProgressEvent;
import org.bspeice.minimalbible.activity.downloader.manager.LocaleManager;
import org.bspeice.minimalbible.activity.downloader.manager.RefreshManager;
import org.crosswire.jsword.book.BookCategory;
@ -23,6 +24,7 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import de.devland.esperandro.Esperandro;
import rx.subjects.PublishSubject;
/**
* Module mappings for the classes under the Download Activity
@ -79,8 +81,15 @@ public class DownloadActivityModules {
@Provides
@Singleton
BookManager provideBookDownloadManager(Books installedBooks, RefreshManager rm) {
return new BookManager(installedBooks, rm);
PublishSubject<DLProgressEvent> dlProgressEventPublisher() {
return PublishSubject.create();
}
@Provides
@Singleton
BookManager provideBookDownloadManager(Books installedBooks, RefreshManager rm,
PublishSubject<DLProgressEvent> progressEvents) {
return new BookManager(installedBooks, rm, progressEvents);
}
@Provides

View File

@ -12,33 +12,41 @@ import rx.Observable;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
import org.crosswire.jsword.book.BookException
import org.crosswire.jsword.util.IndexDownloader
import org.crosswire.common.progress.Progress
/**
* Single point of authority for what is being downloaded and its progress
* Please note that you should never be modifying installedBooks,
* only operate on installedBooksList
*/
//TODO: Install indexes for Bibles
//TODO: Figure out how to get Robolectric to mock the Log, rather than removing the calls
class BookManager(private val installedBooks: Books, val rM: RefreshManager) :
class BookManager(private val installedBooks: Books,
val rM: RefreshManager,
val downloadEvents: PublishSubject<DLProgressEvent>) :
WorkListener, BooksListener {
private val bookJobNamePrefix = Progress.INSTALL_BOOK.substringBeforeLast("%s")
private val indexJobNamePrefix = Progress.DOWNLOAD_SEARCH_INDEX.substringBeforeLast("%s")
/**
* List of jobs currently active by their job name
*/
val inProgressJobNames: MutableMap<String, Book> = hashMapOf()
/**
* Cached copy of downloads in progress so views displaying this info can get it quickly.
*/
// TODO: Combine to one map
val bookMappings: MutableMap<String, Book> = hashMapOf()
val inProgressDownloads: MutableMap<Book, DLProgressEvent> = hashMapOf()
/**
* A list of books that is locally maintained - installedBooks isn't always up-to-date
*/
val installedBooksList: MutableList<Book> = installedBooks.getBooks() ?: linkedListOf()
val downloadEvents: PublishSubject<DLProgressEvent> = PublishSubject.create();
val installedBooksList: MutableList<Book> = installedBooks.getBooks() ?: linkedListOf();
{
JobManager.addWorkListener(this)
installedBooks.addBooksListener(this)
downloadEvents.subscribe { this.inProgressDownloads[it.b] = it }
}
/**
@ -48,25 +56,35 @@ class BookManager(private val installedBooks: Books, val rM: RefreshManager) :
* @param b The book to predict the download job name of
* @return The name of the job that will/is download/ing this book
*/
fun getJobId(b: Book) = "INSTALL_BOOK-${b.getInitials()}"
fun installBook(b: Book) {
downloadBook(b)
addJob(getJobId(b), b)
downloadEvents onNext DLProgressEvent(DLProgressEvent.PROGRESS_BEGINNING, b)
}
fun addJob(jobId: String, b: Book) {
bookMappings.put(jobId, b)
}
fun getJobNames(b: Book) = listOf("${bookJobNamePrefix}${b.getInitials()}",
"${indexJobNamePrefix}${b.getInitials()}")
fun downloadBook(b: Book) {
// First, look up where the Book came from
Observable.just(rM installerFromBook b)
.observeOn(Schedulers.io())
.subscribe { it subscribe { it install b } }
val installerObs = Observable.just(rM installerFromBook b)
downloadEvents onNext DLProgressEvent(DLProgressEvent.PROGRESS_BEGINNING, b)
// And subscribe on two different threads for the download
// Not sure why we need two threads, guessing it's because the
// thread is closed when the install event is done
installerObs
.observeOn(Schedulers.newThread())
.subscribe {
// Download the actual book
it subscribe { it install b }
}
installerObs
.observeOn(Schedulers.newThread())
.subscribe {
// Download the book index
it subscribe { IndexDownloader.downloadIndex(b, it) }
}
// Then notify everyone that we're starting
downloadEvents onNext DLProgressEvent.beginningEvent(b)
// Finally register the jobs in progress
getJobNames(b).forEach { this.inProgressJobNames[it] = b }
}
/**
@ -100,6 +118,7 @@ class BookManager(private val installedBooks: Books, val rM: RefreshManager) :
return false
}
}
/**
* Check the status of a book download in progress.
* @param b The book to get the current progress of
@ -112,20 +131,24 @@ class BookManager(private val installedBooks: Books, val rM: RefreshManager) :
// TODO: I have a strange feeling I can simplify this further...
override fun workProgressed(ev: WorkEvent) {
val job = ev.getJob()
bookMappings.filter { it.getKey() == job.getJobID() }
.map {
// We multiply by 100 first to avoid integer truncation
// Also avoids roundoff error. Neat trick, but I'm spending just as much time
// documenting it as implementing the floating point would take
val event = DLProgressEvent(job.getWorkDone() * 100 / job.getTotalWork(), it.getValue())
downloadEvents onNext event
if (job.getWorkDone() == job.getTotalWork()) {
inProgressDownloads remove bookMappings[job.getJobID()]
bookMappings remove job.getJobID()
} else
inProgressDownloads.put(it.getValue(), event)
}
val book = inProgressJobNames[job.getJobID()] as Book
val oldEvent = inProgressDownloads[book] ?: DLProgressEvent.beginningEvent(book)
var newEvent: DLProgressEvent
if (job.getJobID().contains(bookJobNamePrefix))
newEvent = oldEvent.copy(bookProgress = job.getWork())
else
newEvent = oldEvent.copy(indexProgress = job.getWork())
downloadEvents onNext newEvent
if (newEvent.averageProgress == DLProgressEvent.PROGRESS_COMPLETE) {
inProgressDownloads remove inProgressJobNames[job.getJobID()]
inProgressJobNames remove job.getJobID()
} else
inProgressDownloads.put(book, newEvent)
}
override fun workStateChanged(ev: WorkEvent) {
@ -133,18 +156,8 @@ class BookManager(private val installedBooks: Books, val rM: RefreshManager) :
}
override fun bookAdded(booksEvent: BooksEvent) {
// It's possible the install finished before we received a progress event for it,
// we handle that case here.
val b = booksEvent.getBook()
// Log.d("BookDownloadManager", "Book added: ${b.getName()}")
inProgressDownloads remove b
// Not sure why, but the inProgressDownloads might not have our book,
// so we always trigger the PROGRESS_COMPLETE event.
downloadEvents onNext DLProgressEvent(DLProgressEvent.PROGRESS_COMPLETE, b)
// And update the locally available list
installedBooksList add b
// Update the local list of available books
installedBooksList add booksEvent.getBook()
}
override fun bookRemoved(booksEvent: BooksEvent) {

View File

@ -5,12 +5,23 @@ import org.crosswire.jsword.book.Book
/**
* Created by bspeice on 11/11/14.
*/
class DLProgressEvent(val progress: Int, val b: Book) {
data class DLProgressEvent(val bookProgress: Int,
val indexProgress: Int,
val b: Book) {
class object {
val PROGRESS_COMPLETE = 100
val PROGRESS_BEGINNING = 0
/**
* Build a DLProgressEvent that is just beginning
* Mostly just a nice shorthand
*/
fun beginningEvent(b: Book) = DLProgressEvent(DLProgressEvent.PROGRESS_BEGINNING,
DLProgressEvent.PROGRESS_BEGINNING, b)
}
fun toCircular() = (progress.toFloat() * 360 / 100).toInt()
val averageProgress: Int
get() = (bookProgress + indexProgress) / 2
fun toCircular() = (averageProgress.toFloat() * 360 / 100).toInt()
}