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 {
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.+'
}

View File

@ -1,22 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.bspeice.minimalbible" >
package="org.bspeice.minimalbible">
<application
android:name=".MinimalBible"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name=".MinimalBible" >
android:theme="@style/AppTheme">
<activity
android:name=".activity.downloader.DownloadActivity"
android:label="@string/app_name" >
</activity>
<activity android:name=".activity.viewer.BibleViewer"
android:label="@string/app_name" >
android:label="@string/app_name"></activity>
<activity
android:name=".activity.viewer.BibleViewer"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

View File

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

View File

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

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