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.
This commit is contained in:
Bradlee Speice 2014-08-09 19:34:31 -04:00
parent 0caca67f44
commit ad33ed9619
6 changed files with 247 additions and 55 deletions

View File

@ -55,20 +55,19 @@ android {
dependencies { dependencies {
compile project(path: ':jsword-minimalbible', configuration: 'buildJSword') compile project(path: ':jsword-minimalbible', configuration: 'buildJSword')
compile 'com.squareup.dagger:dagger:1.2.1' compile 'com.squareup.dagger:dagger:+'
provided 'com.squareup.dagger:dagger-compiler:1.2.1' 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 'com.jayway.awaitility:awaitility:1.6.0'
androidTestCompile 'org.mockito:mockito-core:+' androidTestCompile 'org.mockito:mockito-core:+'
androidTestCompile 'com.google.dexmaker:dexmaker:+' androidTestCompile 'com.google.dexmaker:dexmaker:+'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:+' 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.+'
} }

View File

@ -3,17 +3,16 @@
package="org.bspeice.minimalbible"> package="org.bspeice.minimalbible">
<application <application
android:name=".MinimalBible"
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme" android:theme="@style/AppTheme">
android:name=".MinimalBible" >
<activity <activity
android:name=".activity.downloader.DownloadActivity" android:name=".activity.downloader.DownloadActivity"
android:label="@string/app_name" > android:label="@string/app_name"></activity>
</activity> <activity
android:name=".activity.viewer.BibleViewer"
<activity android:name=".activity.viewer.BibleViewer"
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -12,9 +12,11 @@ import android.webkit.WebViewClient;
import org.bspeice.minimalbible.Injector; import org.bspeice.minimalbible.Injector;
import org.bspeice.minimalbible.R; import org.bspeice.minimalbible.R;
import org.bspeice.minimalbible.activity.BaseFragment; 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.Book;
import org.crosswire.jsword.book.BookMetaData; import org.crosswire.jsword.passage.Verse;
import org.crosswire.jsword.versification.Versification; import org.crosswire.jsword.versification.BibleBook;
import java.util.List; import java.util.List;
@ -23,22 +25,31 @@ import javax.inject.Named;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.InjectView; 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. * A placeholder fragment containing a simple view.
*/ */
public class BookFragment extends BaseFragment { public class BookFragment extends BaseFragment {
@Inject @Named("MainBook") Book mBook;
private static final String ARG_BOOK_NAME = "book_name";
@Inject
@Named("MainBook")
Lazy<Book> mBook;
@Inject
VersificationUtil vUtil;
// TODO: Factory?
VerseLookupService lookupService;
@InjectView(R.id.book_content) @InjectView(R.id.book_content)
WebView mainContent; 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) { public static BookFragment newInstance(String bookName) {
BookFragment fragment = new BookFragment(); BookFragment fragment = new BookFragment();
@ -48,9 +59,6 @@ public class BookFragment extends BaseFragment {
return fragment; return fragment;
} }
public BookFragment() {
}
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
@ -63,28 +71,35 @@ public class BookFragment extends BaseFragment {
View rootView = inflater.inflate(R.layout.fragment_viewer_main, container, View rootView = inflater.inflate(R.layout.fragment_viewer_main, container,
false); false);
((Injector)getActivity()).inject(this); ((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); ButterKnife.inject(this, rootView);
mainContent.getSettings().setJavaScriptEnabled(true); mainContent.getSettings().setJavaScriptEnabled(true);
// TODO: Load initial text from SharedPreferences // TODO: Load initial text from SharedPreferences, rather than getting the actual book.
displayBook(mBook); displayBook(mBook.get());
Log.d("BookFragment", getVersification(mBook).toString());
return rootView; return rootView;
} }
private Versification getVersification(Book b) {
return instance().getVersification((String) b.getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION));
}
// TODO: Remove? // TODO: Remove?
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(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) { private void displayBook(Book b) {
Log.d("BookFragment", b.getName()); Log.d("BookFragment", b.getName());
((BibleViewer)getActivity()).setActionBarTitle(b.getInitials()); ((BibleViewer)getActivity()).setActionBarTitle(b.getInitials());
@ -92,13 +107,30 @@ public class BookFragment extends BaseFragment {
mainContent.setWebViewClient(new WebViewClient(){ mainContent.setWebViewClient(new WebViewClient(){
@Override @Override
public void onPageFinished(WebView view, String url) { 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); 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) { private void invokeJavascript(String function, Object arg) {
mainContent.loadUrl("javascript:" + function + "('" + arg.toString() + "')"); mainContent.loadUrl("javascript:" + function + "('" + arg.toString() + "')");
} }
@ -106,20 +138,4 @@ public class BookFragment extends BaseFragment {
private void invokeJavascript(String function, List<Object> args) { private void invokeJavascript(String function, List<Object> args) {
mainContent.loadUrl("javascript:" + function + "(" + joinString(",", args.toArray()) + ")"); 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();
}
}
} }

View File

@ -22,6 +22,7 @@ public class BookManager {
@Inject @Inject
BookManager() { BookManager() {
// TODO: Any way this can be sped up goes straight to the initialization time.
installedBooks = Observable.from(Books.installed().getBooks()) installedBooks = Observable.from(Books.installed().getBooks())
.cache(); .cache();
installedBooks.subscribeOn(Schedulers.io()) installedBooks.subscribeOn(Schedulers.io())
@ -46,5 +47,4 @@ public class BookManager {
public Boolean isRefreshComplete() { public Boolean isRefreshComplete() {
return refreshComplete; return refreshComplete;
} }
} }

View File

@ -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).
* <p/>
* 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.
* <p/>
* TODO: Statistics on cache hits/misses vs. verses cached
*/
public class VerseLookupService implements Action1<Verse> {
private static final int MAX_SIZE = 1000000; // 1MB
Book book;
LruCache<String, String> 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<Verse> listener = PublishSubject.create();
public VerseLookupService(Book b, Context ctx) {
listener.subscribeOn(Schedulers.io())
.subscribe(this);
this.book = b;
this.cache = new LruCache<String, String>(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 (\<p\/>)
*/
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) {
}
}

View File

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