From ad33ed9619501488a89b302e3b3484bac355c8c3 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 9 Aug 2014 19:34:31 -0400 Subject: [PATCH] Initial verse lookup and caching. Having some weird issues on the build, syncing to Github so I can test elsewhere. Things are likely broken right now. --- app/build.gradle | 19 +-- app/src/main/AndroidManifest.xml | 17 +- .../activity/viewer/BookFragment.java | 86 ++++++---- .../activity/viewer/BookManager.java | 2 +- .../service/book/VerseLookupService.java | 155 ++++++++++++++++++ .../bspeice/minimalbible/util/StringUtil.java | 23 +++ 6 files changed, 247 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/org/bspeice/minimalbible/service/book/VerseLookupService.java create mode 100644 app/src/main/java/org/bspeice/minimalbible/util/StringUtil.java diff --git a/app/build.gradle b/app/build.gradle index 0cc9f85..2f3bd96 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,20 +55,19 @@ android { dependencies { compile project(path: ':jsword-minimalbible', configuration: 'buildJSword') - compile 'com.squareup.dagger:dagger:1.2.1' - provided 'com.squareup.dagger:dagger-compiler:1.2.1' + compile 'com.squareup.dagger:dagger:+' + provided 'com.squareup.dagger:dagger-compiler:+' - provided 'de.devland.esperandro:esperandro:1.1.2' + compile 'de.devland.esperandro:esperandro-api:+' + provided 'de.devland.esperandro:esperandro:+' + + compile 'com.jakewharton:butterknife:+' + compile 'com.readystatesoftware.systembartint:systembartint:+' + compile 'com.netflix.rxjava:rxjava-android:+' + compile 'com.android.support:appcompat-v7:20.+' androidTestCompile 'com.jayway.awaitility:awaitility:1.6.0' androidTestCompile 'org.mockito:mockito-core:+' androidTestCompile 'com.google.dexmaker:dexmaker:+' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:+' - // androidTestProvided 'com.squareup.dagger:dagger-compiler:1.2.0' - - compile 'com.jakewharton:butterknife:5.0.1' - compile 'de.devland.esperandro:esperandro-api:1.1.2' - compile 'com.readystatesoftware.systembartint:systembartint:1.0.3' - compile 'com.netflix.rxjava:rxjava-android:0.19.0' - compile 'com.android.support:appcompat-v7:20.+' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 598523f..1c68650 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,21 @@ + package="org.bspeice.minimalbible"> + android:theme="@style/AppTheme"> - - - + android:label="@string/app_name"> + - + diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookFragment.java b/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookFragment.java index a413b0c..3de3071 100644 --- a/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookFragment.java +++ b/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookFragment.java @@ -12,9 +12,11 @@ import android.webkit.WebViewClient; import org.bspeice.minimalbible.Injector; import org.bspeice.minimalbible.R; import org.bspeice.minimalbible.activity.BaseFragment; +import org.bspeice.minimalbible.activity.viewer.bookutil.VersificationUtil; +import org.bspeice.minimalbible.service.book.VerseLookupService; import org.crosswire.jsword.book.Book; -import org.crosswire.jsword.book.BookMetaData; -import org.crosswire.jsword.versification.Versification; +import org.crosswire.jsword.passage.Verse; +import org.crosswire.jsword.versification.BibleBook; import java.util.List; @@ -23,22 +25,31 @@ import javax.inject.Named; import butterknife.ButterKnife; import butterknife.InjectView; +import dagger.Lazy; -import static org.crosswire.jsword.versification.system.Versifications.instance; +import static org.bspeice.minimalbible.util.StringUtil.joinString; /** * A placeholder fragment containing a simple view. */ public class BookFragment extends BaseFragment { - @Inject @Named("MainBook") Book mBook; + private static final String ARG_BOOK_NAME = "book_name"; + @Inject + @Named("MainBook") + Lazy mBook; + @Inject + VersificationUtil vUtil; + // TODO: Factory? + VerseLookupService lookupService; @InjectView(R.id.book_content) WebView mainContent; - private static final String ARG_BOOK_NAME = "book_name"; + public BookFragment() { + } /** - * Returns a new instance of this fragment for the given section number. + * Returns a new instance of this fragment for the given book. */ public static BookFragment newInstance(String bookName) { BookFragment fragment = new BookFragment(); @@ -48,9 +59,6 @@ public class BookFragment extends BaseFragment { return fragment; } - public BookFragment() { - } - @Override public void onCreate(Bundle state) { super.onCreate(state); @@ -63,28 +71,35 @@ public class BookFragment extends BaseFragment { View rootView = inflater.inflate(R.layout.fragment_viewer_main, container, false); ((Injector)getActivity()).inject(this); + // TODO: Defer lookup until after webview created? When exactly is WebView created? + this.lookupService = new VerseLookupService(mBook.get(), this.getActivity()); ButterKnife.inject(this, rootView); mainContent.getSettings().setJavaScriptEnabled(true); - // TODO: Load initial text from SharedPreferences + // TODO: Load initial text from SharedPreferences, rather than getting the actual book. - displayBook(mBook); - - Log.d("BookFragment", getVersification(mBook).toString()); + displayBook(mBook.get()); return rootView; } - private Versification getVersification(Book b) { - return instance().getVersification((String) b.getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION)); - } - // TODO: Remove? @Override public void onAttach(Activity activity) { super.onAttach(activity); } + /*---------------------------------------- + Here be all the methods you want to spend time with + ---------------------------------------- + */ + + /** + * Do the initial work of displaying a book. Requires setting up WebView, etc. + * TODO: Get initial content from cache? + * + * @param b + */ private void displayBook(Book b) { Log.d("BookFragment", b.getName()); ((BibleViewer)getActivity()).setActionBarTitle(b.getInitials()); @@ -92,13 +107,30 @@ public class BookFragment extends BaseFragment { mainContent.setWebViewClient(new WebViewClient(){ @Override public void onPageFinished(WebView view, String url) { + // TODO: Restore this verse from a SharedPref + Verse initial = new Verse(vUtil.getVersification(mBook.get()), + BibleBook.GEN, 1, 1); super.onPageFinished(view, url); - invokeJavascript("set_content", BookFragment.this.mBook.getName()); + invokeJavascript("set_content", lookupService.getHTMLVerse(initial)); } }); - } + /** + * Do the heavy listing of getting the actual text for a verse + * + * @param v + */ + public void displayVerse(Verse v) { + Book b = mBook.get(); + lookupService.getHTMLVerse(v); + } + + /*----------------------------------------- + Here be the methods you wish didn't have to exist. + ----------------------------------------- + */ + private void invokeJavascript(String function, Object arg) { mainContent.loadUrl("javascript:" + function + "('" + arg.toString() + "')"); } @@ -106,20 +138,4 @@ public class BookFragment extends BaseFragment { private void invokeJavascript(String function, List args) { mainContent.loadUrl("javascript:" + function + "(" + joinString(",", args.toArray()) + ")"); } - - // Convenience from http://stackoverflow.com/a/17795110/1454178 - public static String joinString(String join, Object... strings) { - if (strings == null || strings.length == 0) { - return ""; - } else if (strings.length == 1) { - return strings[0].toString(); - } else { - StringBuilder sb = new StringBuilder(); - sb.append(strings[0]); - for (int i = 1; i < strings.length; i++) { - sb.append(join).append(strings[i].toString()); - } - return sb.toString(); - } - } } diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookManager.java b/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookManager.java index 8bf2108..b1b0e92 100644 --- a/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookManager.java +++ b/app/src/main/java/org/bspeice/minimalbible/activity/viewer/BookManager.java @@ -22,6 +22,7 @@ public class BookManager { @Inject BookManager() { + // TODO: Any way this can be sped up goes straight to the initialization time. installedBooks = Observable.from(Books.installed().getBooks()) .cache(); installedBooks.subscribeOn(Schedulers.io()) @@ -46,5 +47,4 @@ public class BookManager { public Boolean isRefreshComplete() { return refreshComplete; } - } diff --git a/app/src/main/java/org/bspeice/minimalbible/service/book/VerseLookupService.java b/app/src/main/java/org/bspeice/minimalbible/service/book/VerseLookupService.java new file mode 100644 index 0000000..30e7b51 --- /dev/null +++ b/app/src/main/java/org/bspeice/minimalbible/service/book/VerseLookupService.java @@ -0,0 +1,155 @@ +package org.bspeice.minimalbible.service.book; + +import android.content.Context; +import android.support.v4.util.LruCache; + +import org.crosswire.common.xml.Converter; +import org.crosswire.common.xml.TransformingSAXEventProvider; +import org.crosswire.common.xml.XMLUtil; +import org.crosswire.jsword.book.Book; +import org.crosswire.jsword.book.BookData; +import org.crosswire.jsword.book.BookException; +import org.crosswire.jsword.book.BookMetaData; +import org.crosswire.jsword.passage.Verse; +import org.crosswire.jsword.util.ConverterFactory; +import org.xml.sax.SAXException; + +import javax.xml.transform.TransformerException; + +import rx.functions.Action1; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; + +/** + * This class has a simple purpose, but implements the dirty work needed to make it happen. + * The idea is this: someone wants the text for a verse, we look it up quickly. + * This means aggressive caching, cache prediction, and classes letting us know that some verses + * may be needed in the future (i.e. searching). + *

+ * There is one VerseLookupService per Book, but multiple VerseLookupServices can work with + * the same book. Because the actual caching mechanism is disk-based, we're safe. + *

+ * TODO: Statistics on cache hits/misses vs. verses cached + */ +public class VerseLookupService implements Action1 { + + private static final int MAX_SIZE = 1000000; // 1MB + Book book; + LruCache cache; + /** + * The listener is responsible for delegating calls to cache verses. + * This way, @notifyVerse can just tell the listener what's what, + * and the listener can delegate to another thread. + */ + private PublishSubject listener = PublishSubject.create(); + + public VerseLookupService(Book b, Context ctx) { + listener.subscribeOn(Schedulers.io()) + .subscribe(this); + this.book = b; + this.cache = new LruCache(MAX_SIZE); + } + + /** + * Get the text for a corresponding verse + * First, check the cache. If that doesn't exist, manually get the verse. + * In all cases, notify that we're looking up a verse so we can get the surrounding ones. + * + * @param v The verse to look up + * @return The HTML text for this verse (\) + */ + public String getHTMLVerse(Verse v) { + if (contains(v)) { + return cache.get(getEntryName(v)); + } else { + // The awkward method calls below are so notifyVerse doesn't + // call the same doVerseLookup + String verseContent = doVerseLookup(v); + notifyVerse(v); + return verseContent; + } + } + + /** + * Perform the ugly work of getting the actual data for a verse + */ + public String doVerseLookup(Verse v) { + BookData bookData = new BookData(book, v); + + String verseHTML = null; + + try { + Converter styler = ConverterFactory.getConverter(); + TransformingSAXEventProvider htmlsep = (TransformingSAXEventProvider) + styler.convert(bookData.getSAXEventProvider()); + BookMetaData bmd = book.getBookMetaData(); + boolean direction = bmd.isLeftToRight(); + htmlsep.setParameter("direction", direction ? "ltr" : "rtl"); + + verseHTML = XMLUtil.writeToString(htmlsep); + } catch (TransformerException e) { + e.printStackTrace(); + } catch (BookException e) { + e.printStackTrace(); + } catch (SAXException e) { + e.printStackTrace(); + } + + return verseHTML; + } + + /** + * Not necessary, but helpful if you let us know ahead of time we should pre-cache a verse. + * For example, if something showed up in search results, it'd be helpful to start + * looking up some of the results. + * + * @param v The verse we should pre-cache + */ + public void notifyVerse(Verse v) { + listener.onNext(v); + } + + /** + * Let someone know if the cache contains a verse we want + * Also provides a nice wrapper if the underlying cache isn't working properly. + * + * @param v The verse to check + * @return Whether we can retrieve the verse from our cache + */ + public boolean contains(Verse v) { + return cache.get(getEntryName(v)) != null; + } + + /** + * Given a verse, what should it's name in the cache be? + * Example: Matthew 7:7 becomes: + * MAT_7_7 + * + * @param v The verse we need to generate a name for + * @return The name this verse should have in the cache + */ + private String getEntryName(Verse v) { + StringBuilder sb = new StringBuilder(); + sb.append(v.getBook().toString() + "_"); + sb.append(v.getChapter() + "_"); + sb.append(v.getVerse()); + return sb.toString(); + } + + /*------------------------------------------------------------------------ + IO Thread operations below + ------------------------------------------------------------------------*/ + + /** + * The listener has let us know that we need to look up a verse. So, look up + * that one first, and get its surrounding verses as well just in case. + * We can safely assume we are not on the main thread. + * + * @param verse The verse we need to look up + */ + + @Override + public void call(Verse verse) { + + } +} diff --git a/app/src/main/java/org/bspeice/minimalbible/util/StringUtil.java b/app/src/main/java/org/bspeice/minimalbible/util/StringUtil.java new file mode 100644 index 0000000..c969b2c --- /dev/null +++ b/app/src/main/java/org/bspeice/minimalbible/util/StringUtil.java @@ -0,0 +1,23 @@ +package org.bspeice.minimalbible.util; + +/** + * Created by bspeice on 8/3/14. + */ +public class StringUtil { + + // Convenience from http://stackoverflow.com/a/17795110/1454178 + public static String joinString(String join, Object... strings) { + if (strings == null || strings.length == 0) { + return ""; + } else if (strings.length == 1) { + return strings[0].toString(); + } else { + StringBuilder sb = new StringBuilder(); + sb.append(strings[0]); + for (int i = 1; i < strings.length; i++) { + sb.append(join).append(strings[i].toString()); + } + return sb.toString(); + } + } +}