package yuku.alkitab.base.storage; import android.util.Log; import yuku.alkitab.base.App; import yuku.alkitab.base.config.AppConfig; import yuku.alkitab.io.BibleReader; import yuku.alkitab.model.Book; import yuku.alkitab.model.FootnoteEntry; import yuku.alkitab.model.InternalBook; import yuku.alkitab.model.PericopeBlock; import yuku.alkitab.model.SingleChapterVerses; import yuku.alkitab.model.XrefEntry; import yuku.alkitab.util.Ari; import yuku.alkitab.yes1.Yes1PericopeIndex; import yuku.alkitab.yes2.io.RandomInputStream; import yuku.alkitab.yes2.section.FootnotesSection; import yuku.alkitab.yes2.section.XrefsSection; import yuku.bintex.BintexReader; import java.io.IOException; import java.io.InputStream; public class InternalReader implements BibleReader { public static final String TAG = InternalReader.class.getSimpleName(); // # for asset cache private static InputStream cache_inputStream = null; private static String cache_file = null; private static int cache_posInput = -1; private final String versionPrefix; private final String versionLocale; private final String versionShortName; private final String versionLongName; private final VerseTextDecoder verseTextDecoder; private Yes1PericopeIndex pericopeIndex_; private XrefsSection xrefsSection_; private boolean xrefsKnownNotAvailable; private FootnotesSection footnotesSection_; private boolean footnotesKnownNotAvailable; public InternalReader(String versionPrefix, String versionLocale, String versionShortName, String versionLongName, VerseTextDecoder verseTextDecoder) { this.versionPrefix = versionPrefix; this.versionLocale = versionLocale; this.versionShortName = versionShortName; this.versionLongName = versionLongName; this.verseTextDecoder = verseTextDecoder; } static class InternalSingleChapterVerses extends SingleChapterVerses { private final String[] verses; public InternalSingleChapterVerses(String[] verses) { this.verses = verses; } @Override public String getVerse(int verse_0) { return verses[verse_0]; } @Override public int getVerseCount() { return verses.length; } } @Override public String getLocale() { return versionLocale; } @Override public String getShortName() { return versionShortName; } @Override public String getLongName() { return versionLongName; } @Override public String getDescription() { return null; } @Override public Book[] loadBooks() { BintexReader br = null; try { final InputStream is = App.context.getAssets().open("internal/" + versionPrefix + "_index_bt.bt"); br = new BintexReader(is); // uint8 version = 3 // uint8 book_count final int version = br.readUint8(); if (version != 3) throw new RuntimeException("Internal index version not supported: " + version); final int book_count = br.readUint8(); final Book[] res = new Book[book_count]; for (int i = 0; i < book_count; i++) { res[i] = readBook(br); } return res; } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) br.close(); } } private static InternalBook readBook(final BintexReader br) throws IOException { // uint8 bookId; // value<string> shortName // value<string> abbreviation // value<string> resName // uint8 chapter_count // uint8[chapter_count] verse_counts // varuint[chapter_count+1] chapter_offsets final InternalBook res = new InternalBook(); res.bookId = br.readUint8(); res.shortName = br.readValueString(); res.abbreviation = br.readValueString(); res.resName = br.readValueString(); res.chapter_count = br.readUint8(); res.verse_counts = new int[res.chapter_count]; for (int i = 0; i < res.chapter_count; i++) { res.verse_counts[i] = br.readUint8(); } res.chapter_offsets = new int[res.chapter_count + 1]; for (int i = 0; i < res.chapter_count + 1; i++) { res.chapter_offsets[i] = br.readVarUint(); } return res; } @Override public SingleChapterVerses loadVerseText(Book book, int chapter_1, boolean dontSplitVerses, boolean lowercased) { InternalBook internalBook = (InternalBook) book; if (chapter_1 < 1 || chapter_1 > book.chapter_count) { return null; } int offset = internalBook.chapter_offsets[chapter_1 - 1]; try { InputStream in; if (cache_inputStream == null) { // case 1: haven't opened anything in = App.context.getAssets().open("internal/" + internalBook.resName + ".txt"); cache_inputStream = in; cache_file = internalBook.resName; //noinspection ResultOfMethodCallIgnored in.skip(offset); cache_posInput = offset; } else { // case 2: we have ever opened. Check if the file is the same if (internalBook.resName.equals(cache_file)) { // case 2.1: yes the file was the same if (offset >= cache_posInput) { // we can go forward in = cache_inputStream; //noinspection ResultOfMethodCallIgnored in.skip(offset - cache_posInput); cache_posInput = offset; } else { // but can't go backward, so we close the stream and reopen it cache_inputStream.close(); in = App.context.getAssets().open("internal/" + internalBook.resName + ".txt"); cache_inputStream = in; //noinspection ResultOfMethodCallIgnored in.skip(offset); cache_posInput = offset; } } else { // case 2.2: different file. So close current and open the new one cache_inputStream.close(); in = App.context.getAssets().open("internal/" + internalBook.resName + ".txt"); cache_inputStream = in; cache_file = internalBook.resName; //noinspection ResultOfMethodCallIgnored in.skip(offset); cache_posInput = offset; } } final int length; if (chapter_1 == internalBook.chapter_count) { length = in.available(); } else { length = internalBook.chapter_offsets[chapter_1] - offset; } byte[] ba = new byte[length]; in.read(ba); cache_posInput += ba.length; // do not close even though we finished reading. The asset file could be the same as before. if (dontSplitVerses) { return new InternalSingleChapterVerses(new String[] { verseTextDecoder.makeIntoSingleString(ba, lowercased) }); } else { return new InternalSingleChapterVerses(verseTextDecoder.separateIntoVerses(ba, lowercased)); } } catch (IOException e) { return new InternalSingleChapterVerses(new String[] { e.getMessage() }); } } private Yes1PericopeIndex loadPericopeIndex() { if (pericopeIndex_ != null) { return pericopeIndex_; } final long startTime = System.currentTimeMillis(); final InputStream is; try { is = App.context.getAssets().open("internal/" + versionPrefix + "_pericope_index_bt.bt"); } catch (IOException e) { return null; } BintexReader in = new BintexReader(is); try { pericopeIndex_ = Yes1PericopeIndex.read(in); return pericopeIndex_; } catch (IOException e) { Log.e(TAG, "Error reading pericope index", e); return null; } finally { in.close(); Log.d(TAG, "Read pericope index needed: " + (System.currentTimeMillis() - startTime)); } } @Override public int loadPericope(int bookId, int chapter_1, int[] aris, PericopeBlock[] pericopeBlocks, int max) { Yes1PericopeIndex pericopeIndex = loadPericopeIndex(); if (pericopeIndex == null) { return 0; // no pericopes! } int ariMin = Ari.encode(bookId, chapter_1, 0); int ariMax = Ari.encode(bookId, chapter_1 + 1, 0); int res = 0; int pertama = pericopeIndex.findFirst(ariMin, ariMax); if (pertama == -1) { return 0; } int kini = pertama; BintexReader in = null; try { in = new BintexReader(App.context.getAssets().open("internal/" + versionPrefix + "_pericope_blocks_bt.bt")); while (true) { int ari = pericopeIndex.getAri(kini); if (ari >= ariMax) { // That's all. No longer relevant. break; } PericopeBlock pericopeBlock = pericopeIndex.getBlock(in, kini); kini++; if (res < max) { aris[res] = ari; pericopeBlocks[res] = pericopeBlock; res++; } else { break; } } } catch (IOException e) { throw new RuntimeException(e); } finally { if (in != null) in.close(); } return res; } @Override public XrefEntry getXrefEntry(int arif) { if (xrefsKnownNotAvailable) return null; if (xrefsSection_ == null) { final String assetName = "internal/" + AppConfig.get().internalPrefix + "_xrefs_bt.bt"; try { App.context.getAssets().list(assetName); } catch (IOException e) { Log.d(TAG, "Can't load xrefs from internal, marking it as not available."); xrefsKnownNotAvailable = true; return null; } try { xrefsSection_ = new XrefsSection.Reader().read(new AssetRandomInputStream(assetName)); } catch (IOException e) { throw new RuntimeException("Error reading xrefs section from internal", e); } } return xrefsSection_.getXrefEntry(arif); } @Override public FootnoteEntry getFootnoteEntry(int arif) { if (footnotesKnownNotAvailable) return null; if (footnotesSection_ == null) { final String assetName = "internal/" + AppConfig.get().internalPrefix + "_footnotes_bt.bt"; try { App.context.getAssets().list(assetName); } catch (IOException e) { Log.d(TAG, "Can't load footnotes from internal, marking it as not available."); footnotesKnownNotAvailable = true; return null; } try { footnotesSection_ = new FootnotesSection.Reader().read(new AssetRandomInputStream(assetName)); } catch (Exception e) { Log.e(TAG, "Error reading footnotes section from internal", e); return null; } } return footnotesSection_.getFootnoteEntry(arif); } } class AssetRandomInputStream extends RandomInputStream { final String assetName; InputStream in; int pos; public AssetRandomInputStream(final String assetName) { this.assetName = assetName; reopen(); } private void reopen() { try { this.in = App.context.getAssets().open(assetName); } catch (IOException e) { throw new RuntimeException(e); } this.pos = 0; } @Override public int read() throws IOException { final int res = in.read(); if (res >= 0) { pos++; } return res; } @Override public int read(byte[] buffer) throws IOException { final int read = in.read(buffer); pos += read; return read; } @Override public int read(byte[] buffer, int offset, int length) throws IOException { final int read = in.read(buffer, offset, length); pos += read; return read; } @Override public long skip(long n) throws IOException { final long read = in.skip(n); pos += (int) read; return read; } @Override public void seek(long n) throws IOException { if (n >= pos) { //noinspection ResultOfMethodCallIgnored skip(n - pos); } else { reopen(); //noinspection ResultOfMethodCallIgnored skip(n); } } @Override public long getFilePointer() throws IOException { return pos; } @Override public void close() throws IOException { // NOP, no need to close asset } }