diff --git a/Alkitab/build.gradle b/Alkitab/build.gradle index ef91d46d8..556aa98c7 100644 --- a/Alkitab/build.gradle +++ b/Alkitab/build.gradle @@ -17,8 +17,8 @@ android { applicationId 'yuku.alkitab.debug' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 14000263 - versionName '4.4.0-beta3' + versionCode 14000264 + versionName '4.4.0-beta4' multiDexEnabled true } buildTypes { diff --git a/Alkitab/src/main/assets/version_config.json b/Alkitab/src/main/assets/version_config.json index 251cad80b..459780796 100644 --- a/Alkitab/src/main/assets/version_config.json +++ b/Alkitab/src/main/assets/version_config.json @@ -6,7 +6,7 @@ {"locale": "ban", "preset_name": "ban-bali90", "shortName": "BALI90", "longName": "Bali 1990", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bali (Bahasa Sehari-hari) - Cakepan Suci"}, {"locale": "ptu", "preset_name": "ptu-bambam", "shortName": "BAMBAM", "longName": "Bambam", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bambam"}, {"locale": "btd", "preset_name": "btd-dairi", "shortName": "DAIRI", "longName": "Batak-Dairi", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Dairi"}, - {"locale": "btx", "preset_name": "btx-karo", "shortName": "KARO", "longName": "Batak-Karo", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Karo"}, + {"locale": "btx", "preset_name": "btx-karo", "shortName": "KARO", "longName": "Pustaka Si Badia", "modifyTime": 1401926400, "description": "Alkitab Bahasa Karo"}, {"locale": "bbc", "preset_name": "bbc-toba", "shortName": "TOBA", "longName": "Batak-Toba", "modifyTime": 1431043200, "description": "Alkitab Batak Toba"}, {"locale": "bts", "preset_name": "bts-simalungun", "shortName": "SIMA", "longName": "Batak-Simalungun", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Simalungun"}, {"locale": "bug", "preset_name": "bug-bugis", "shortName": "BUGIS", "longName": "Bugis", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bugis"}, @@ -468,7 +468,7 @@ "ban": "Bali", "ptu": "Bambam", "btd": "Batak-Dairi", - "btx": "Batak-Karo", + "btx": "Karo", "bbc": "Batak-Toba", "bts": "Batak-Simalungun", "bug": "Bugis", diff --git a/Alkitab/src/main/java/yuku/alkitab/base/IsiActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/IsiActivity.java index 372f738e4..020974b7e 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/IsiActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/IsiActivity.java @@ -1649,7 +1649,7 @@ private void displaySplitFollowingMaster(int verse_1) { } static boolean loadChapterToVersesView(VersesView versesView, Version version, Book book, int chapter_1, int current_chapter_1, boolean uncheckAllVerses) { - SingleChapterVerses verses = version.loadChapterText(book, chapter_1); + final SingleChapterVerses verses = version.loadChapterText(book, chapter_1); if (verses == null) { return false; } diff --git a/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkerListActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkerListActivity.java index 5fbfe2012..3fbeb21ff 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkerListActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkerListActivity.java @@ -1,6 +1,5 @@ package yuku.alkitab.base.ac; -import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -97,7 +96,6 @@ public static Intent createIntent(Context context, Marker.Kind filter_kind, long return res; } - @SuppressLint("MissingSuperCall") @Override protected void onCreate(Bundle savedInstanceState) { enableNonToolbarUpButton(); @@ -258,7 +256,7 @@ static class FilterResult { String query; boolean needFilter; List filteredMarkers; - String[] tokens; + SearchEngine.ReadyTokens rt; } final Debouncer filter = new Debouncer(200) { @@ -283,18 +281,17 @@ public FilterResult process(@Nullable final String payload) { tokens = null; } else { tokens = QueryTokenizer.tokenize(query); - for (int i = 0; i < tokens.length; i++) { - tokens[i] = tokens[i].toLowerCase(Locale.getDefault()); - } } - final List filteredMarkers = filterEngine(allMarkers, filter_kind, tokens); + final SearchEngine.ReadyTokens rt = tokens == null || tokens.length == 0 ? null : new SearchEngine.ReadyTokens(tokens); + + final List filteredMarkers = filterEngine(allMarkers, filter_kind, rt); final FilterResult res = new FilterResult(); res.query = query; res.needFilter = needFilter; res.filteredMarkers = filteredMarkers; - res.tokens = tokens; + res.rt = rt; return res; } @@ -307,7 +304,7 @@ public void onResult(final FilterResult result) { } setTitleAndNothingText(); - adapter.setData(result.filteredMarkers, result.tokens); + adapter.setData(result.filteredMarkers, result.rt); } }; @@ -505,12 +502,13 @@ void sort(String column, boolean ascending, int columnId) { }; /** - * The real work of filtering happens here + * The real work of filtering happens here. + * @param rt Tokens have to be already lowercased. */ - public static List filterEngine(List allMarkers, Marker.Kind filter_kind, String[] tokens) { - List res = new ArrayList<>(); + public static List filterEngine(List allMarkers, Marker.Kind filter_kind, @Nullable SearchEngine.ReadyTokens rt) { + final List res = new ArrayList<>(); - if (tokens == null || tokens.length == 0) { + if (rt == null) { res.addAll(allMarkers); return res; } @@ -518,7 +516,7 @@ public static List filterEngine(List allMarkers, Marker.Kind fil for (final Marker marker : allMarkers) { if (filter_kind != Marker.Kind.highlight) { // "caption" in highlights only stores color information, so it's useless to check String caption_lc = marker.caption.toLowerCase(Locale.getDefault()); - if (SearchEngine.satisfiesQuery(caption_lc, tokens)) { + if (SearchEngine.satisfiesTokens(caption_lc, rt)) { res.add(marker); continue; } @@ -528,7 +526,7 @@ public static List filterEngine(List allMarkers, Marker.Kind fil String verseText = S.activeVersion.loadVerseText(marker.ari); if (verseText != null) { // this can be null! so beware. String verseText_lc = verseText.toLowerCase(Locale.getDefault()); - if (SearchEngine.satisfiesQuery(verseText_lc, tokens)) { + if (SearchEngine.satisfiesTokens(verseText_lc, rt)) { res.add(marker); } } @@ -540,7 +538,7 @@ public static List filterEngine(List allMarkers, Marker.Kind fil class MarkerListAdapter extends EasyAdapter { List filteredMarkers = new ArrayList<>(); - String[] tokens; + SearchEngine.ReadyTokens rt; @Override public Marker getItem(final int position) { @@ -597,9 +595,9 @@ public void bindView(final View view, final int position, final ViewGroup parent } if (filter_kind == Marker.Kind.bookmark) { - lCaption.setText(currentlyUsedFilter != null ? SearchEngine.hilite(caption, tokens, hiliteColor) : caption); + lCaption.setText(currentlyUsedFilter != null ? SearchEngine.hilite(caption, rt, hiliteColor) : caption); Appearances.applyMarkerTitleTextAppearance(lCaption); - CharSequence snippet = currentlyUsedFilter != null ? SearchEngine.hilite(verseText, tokens, hiliteColor) : verseText; + CharSequence snippet = currentlyUsedFilter != null ? SearchEngine.hilite(verseText, rt, hiliteColor) : verseText; Appearances.applyMarkerSnippetContentAndAppearance(lSnippet, reference, snippet); @@ -617,14 +615,14 @@ public void bindView(final View view, final int position, final ViewGroup parent } else if (filter_kind == Marker.Kind.note) { lCaption.setText(reference); Appearances.applyMarkerTitleTextAppearance(lCaption); - lSnippet.setText(currentlyUsedFilter != null ? SearchEngine.hilite(caption, tokens, hiliteColor) : caption); + lSnippet.setText(currentlyUsedFilter != null ? SearchEngine.hilite(caption, rt, hiliteColor) : caption); Appearances.applyTextAppearance(lSnippet); } else if (filter_kind == Marker.Kind.highlight) { lCaption.setText(reference); Appearances.applyMarkerTitleTextAppearance(lCaption); - final SpannableStringBuilder snippet = currentlyUsedFilter != null ? SearchEngine.hilite(verseText, tokens, hiliteColor) : new SpannableStringBuilder(verseText); + final SpannableStringBuilder snippet = currentlyUsedFilter != null ? SearchEngine.hilite(verseText, rt, hiliteColor) : new SpannableStringBuilder(verseText); final Highlights.Info info = Highlights.decode(caption); if (info != null) { final BackgroundColorSpan span = new BackgroundColorSpan(Highlights.alphaMix(info.colorRgb)); @@ -644,9 +642,9 @@ public int getCount() { return filteredMarkers.size(); } - public void setData(List filteredMarkers, String[] tokens) { + public void setData(List filteredMarkers, SearchEngine.ReadyTokens rt) { this.filteredMarkers = filteredMarkers; - this.tokens = tokens; + this.rt = rt; // set up empty view to make sure it does not show loading progress again tEmpty.setVisibility(View.VISIBLE); diff --git a/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkersActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkersActivity.java index 2e9ded2cd..9a2e38fd8 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkersActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/MarkersActivity.java @@ -267,8 +267,7 @@ public void onReceive(final Context context, final Intent intent) { final File file = new File(result.firstFilename); try { final FileInputStream fis = new FileInputStream(file); - BookmarkImporter.importBookmarks(this, fis, false); - adapter.reload(); + BookmarkImporter.importBookmarks(this, fis, false, () -> adapter.reload()); } catch (IOException e) { new AlertDialogWrapper.Builder(this) .setMessage(R.string.marker_migrate_error_opening_backup_file) diff --git a/Alkitab/src/main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java index 89a6eea6d..ee905156f 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.ColorStateList; import android.net.Uri; import android.os.Bundle; import android.support.v4.widget.DrawerLayout; @@ -68,7 +69,12 @@ public class ReadingPlanActivity extends BaseLeftDrawerActivity implements LeftD private List downloadedReadingPlanInfos; private int todayNumber; private int dayNumber; - private IntArrayList readingCodes; + + /** + * List of reading codes that is read for the current reading plan. + * A reading code is a combination of day (left-bit-shifted by 8) and the reading sequence for that day starting from 0. + */ + private IntArrayList readReadingCodes; private boolean newDropDownItems; private ImageButton bLeft; @@ -349,7 +355,7 @@ private void loadReadingPlanProgress() { if (readingPlan == null) { return; } - readingCodes = S.getDb().getAllReadingCodesByReadingPlanProgressGid(ReadingPlan.gidFromName(readingPlan.info.name)); + readReadingCodes = S.getDb().getAllReadingCodesByReadingPlanProgressGid(ReadingPlan.gidFromName(readingPlan.info.name)); } public void goToIsiActivity(final int dayNumber, final int sequence) { @@ -456,7 +462,7 @@ public void prepareDisplay() { private int findFirstUnreadDay() { for (int i = 0; i < readingPlan.info.duration - 1; i++) { boolean[] readMarks = new boolean[readingPlan.dailyVerses[i].length / 2]; - ReadingPlanManager.writeReadMarksByDay(readingCodes, readMarks, i); + ReadingPlanManager.writeReadMarksByDay(readReadingCodes, readMarks, i); for (boolean readMark : readMarks) { if (!readMark) { return i; @@ -635,32 +641,27 @@ void onReadingPlanDownloadFinished(final byte[] data) { } private float getActualPercentage() { - return 100.f * countRead() / countAllReadings(); + return 100.f * readReadingCodes.size() / countAllReadings(); } private float getTargetPercentage() { return 100.f * countTarget() / countAllReadings(); } - private int countRead() { - IntArrayList filteredReadingCodes = ReadingPlanManager.filterReadingCodesByDayStartEnd(readingCodes, 0, todayNumber); - return filteredReadingCodes.size(); - } - private int countTarget() { - int res = 0; + int doubledCount = 0; for (int i = 0; i <= todayNumber; i++) { - res += readingPlan.dailyVerses[i].length / 2; + doubledCount += readingPlan.dailyVerses[i].length; } - return res; + return doubledCount / 2; } private int countAllReadings() { - int res = 0; + int doubledCount = 0; for (int i = 0; i < readingPlan.info.duration; i++) { - res += readingPlan.dailyVerses[i].length / 2; + doubledCount += readingPlan.dailyVerses[i].length; } - return res; + return doubledCount / 2; } public String getReadingDateHeader(final int dayNumber) { @@ -688,6 +689,7 @@ public void onPositive(final MaterialDialog dialog) { class ReadingPlanAdapter extends EasyAdapter { private int[] todayReadings; + ColorStateList originalCommentTextColor = null; public void load() { if (readingPlan == null) { @@ -730,7 +732,7 @@ public void bindView(final View res, final int position, final ViewGroup parent) final CheckBox checkbox = V.get(res, R.id.checkbox); final boolean[] readMarks = new boolean[todayReadings.length / 2]; - ReadingPlanManager.writeReadMarksByDay(readingCodes, readMarks, dayNumber); + ReadingPlanManager.writeReadMarksByDay(readReadingCodes, readMarks, dayNumber); bReference.setText(S.activeVersion.referenceRange(todayReadings[position * 2], todayReadings[position * 2 + 1])); @@ -767,16 +769,21 @@ public void bindView(final View res, final int position, final ViewGroup parent) tActual.setText(getString(R.string.rp_commentActual, String.format("%.2f", actualPercentage))); tTarget.setText(getString(R.string.rp_commentTarget, String.format("%.2f", targetPercentage))); - String comment; + if (originalCommentTextColor == null) { + originalCommentTextColor = tComment.getTextColors(); + } + if (actualPercentage == targetPercentage) { - comment = getString(R.string.rp_commentOnSchedule); + tComment.setText(R.string.rp_commentOnSchedule); + tComment.setTextColor(getResources().getColor(R.color.escape)); + } else if (actualPercentage < targetPercentage) { + tComment.setText(getString(R.string.rp_commentBehindSchedule, String.format(Locale.US, "%.2f", targetPercentage - actualPercentage))); + tComment.setTextColor(originalCommentTextColor); } else { - String diff = String.format(Locale.US, "%.2f", targetPercentage - actualPercentage); - comment = getString(R.string.rp_commentBehindSchedule, diff); + tComment.setText(getString(R.string.rp_commentAheadSchedule, String.format(Locale.US, "%.2f", actualPercentage - targetPercentage))); + tComment.setTextColor(getResources().getColor(R.color.escape)); } - tComment.setText(comment); - tDetail.setOnClickListener(v -> { showDetails = !showDetails; if (showDetails) { @@ -811,7 +818,7 @@ public void bindView(final View res, final int position, final ViewGroup parent) } final boolean[] readMarks = new boolean[checkbox_count]; - ReadingPlanManager.writeReadMarksByDay(readingCodes, readMarks, day); + ReadingPlanManager.writeReadMarksByDay(readReadingCodes, readMarks, day); for (int i = 0; i < checkbox_count; i++) { final int sequence = i; diff --git a/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java index 6cd5eab7c..edf9d17af 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java @@ -6,6 +6,9 @@ import android.database.MatrixCursor; import android.os.AsyncTask; import android.os.Bundle; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.graphics.ColorUtils; import android.support.v4.widget.CursorAdapter; @@ -15,6 +18,7 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.text.style.UnderlineSpan; import android.util.SparseBooleanArray; import android.view.Menu; @@ -45,6 +49,7 @@ import yuku.alkitab.base.util.Jumper; import yuku.alkitab.base.util.QueryTokenizer; import yuku.alkitab.base.util.SearchEngine; +import yuku.alkitab.debug.BuildConfig; import yuku.alkitab.debug.R; import yuku.alkitab.model.Book; import yuku.alkitab.model.Version; @@ -56,13 +61,17 @@ import java.util.Arrays; import java.util.List; +import static yuku.alkitab.base.util.Literals.Array; + public class SearchActivity extends BaseActivity { public static final String TAG = SearchActivity.class.getSimpleName(); private static final String EXTRA_openedBookId = "openedBookId"; private static int REQCODE_bookFilter = 1; - final String COLUMN_QUERY_STRING = "query_string"; + private static final long ID_CLEAR_HISTORY = -1L; + private static final int COLINDEX_ID = 0; + private static final int COLINDEX_QUERY_STRING = 1; View root; TextView bVersion; @@ -204,16 +213,27 @@ public View newView(final Context context, final Cursor cursor, final ViewGroup @Override public void bindView(final View view, final Context context, final Cursor cursor) { - TextView text1 = V.get(view, android.R.id.text1); - text1.setText(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_QUERY_STRING))); + final TextView text1 = V.get(view, android.R.id.text1); + final long _id = cursor.getLong(COLINDEX_ID); + + final CharSequence text; + if (_id == -1) { + final SpannableStringBuilder sb = new SpannableStringBuilder(getString(R.string.search_clear_history)); + sb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.escape)), 0, sb.length(), 0); + text = sb; + } else { + text = cursor.getString(COLINDEX_QUERY_STRING); + } + + text1.setText(text); } @Override public CharSequence convertToString(final Cursor cursor) { - return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_QUERY_STRING)); + return cursor.getString(COLINDEX_QUERY_STRING); } - public void setData(final SearchHistory searchHistory) { + public void setData(@NonNull final SearchHistory searchHistory) { entries.clear(); entries.addAll(searchHistory.entries); filter(); @@ -225,14 +245,19 @@ public void setQuery(final String query_string) { } private void filter() { - final MatrixCursor mc = new MatrixCursor(new String[]{"_id", COLUMN_QUERY_STRING}); + final MatrixCursor mc = new MatrixCursor(Array("_id", "query_string") /* Can be any string, but this must correspond to COLINDEX_ID and COLINDEX_QUERY_STRING */); for (int i = 0; i < entries.size(); i++) { final SearchHistory.Entry entry = entries.get(i); if (TextUtils.isEmpty(query_string) || entry.query_string.toLowerCase().startsWith(query_string.toLowerCase())) { - mc.addRow(new Object[]{(long) i, entry.query_string}); + mc.addRow(Array((long) i, entry.query_string)); } } + // add last item to clear search history only if there is something else + if (mc.getCount() > 0) { + mc.addRow(Array(ID_CLEAR_HISTORY, "")); + } + // sometimes this is called from bg. So we need to make sure this is run on UI thread. runOnUiThread(() -> swapCursor(mc)); } @@ -291,7 +316,13 @@ public boolean onSuggestionClick(final int position) { final boolean ok = c.moveToPosition(position); if (!ok) return false; - searchView.setQuery(c.getString(c.getColumnIndexOrThrow(COLUMN_QUERY_STRING)), true); + final long _id = c.getLong(COLINDEX_ID); + if (_id == ID_CLEAR_HISTORY) { + saveSearchHistory(null); + searchHistoryAdapter.setData(loadSearchHistory()); + } else { + searchView.setQuery(c.getString(COLINDEX_QUERY_STRING), true); + } return true; } @@ -654,16 +685,31 @@ protected void search(final String query_string) { .show(); new AsyncTask() { + boolean debugstats_revIndexUsed; + long debugstats_totalTimeMs; + long debugstats_cpuTimeMs; + @Override protected IntArrayList doInBackground(Void... params) { searchHistoryAdapter.setData(addSearchHistoryEntry(query_string)); + final long totalMs = System.currentTimeMillis(); + final long cpuMs = SystemClock.currentThreadTimeMillis(); + final IntArrayList res; + synchronized (SearchActivity.this) { if (usingRevIndex()) { - return SearchEngine.searchByRevIndex(searchInVersion, getQuery()); + debugstats_revIndexUsed = true; + res = SearchEngine.searchByRevIndex(searchInVersion, getQuery()); } else { - return SearchEngine.searchByGrep(searchInVersion, getQuery()); + debugstats_revIndexUsed = false; + res = SearchEngine.searchByGrep(searchInVersion, getQuery()); } } + + debugstats_totalTimeMs = System.currentTimeMillis() - totalMs; + debugstats_cpuTimeMs = SystemClock.currentThreadTimeMillis() - cpuMs; + + return res; } @Override protected void onPostExecute(IntArrayList result) { @@ -716,6 +762,17 @@ protected void search(final String query_string) { tSearchTips.setOnClickListener(null); } } + + if (BuildConfig.DEBUG) { + new MaterialDialog.Builder(SearchActivity.this) + .content("This msg is shown only on DEBUG build\n\n" + + "Search results: " + result.size() + "\n" + + "Method: " + (debugstats_revIndexUsed? "revindex": "grep") + "\n" + + "Total time: " + debugstats_totalTimeMs + " ms\n" + + "CPU (thread) time: " + debugstats_cpuTimeMs + " ms") + .positiveText(R.string.ok) + .show(); + } pd.setOnDismissListener(null); pd.dismiss(); @@ -750,7 +807,7 @@ int shouldShowFallback(final Jumper jumper) { }.execute(); } - SearchHistory loadSearchHistory() { + @NonNull SearchHistory loadSearchHistory() { final String json = Preferences.getString(Prefkey.searchHistory, null); if (json == null) { return new SearchHistory(); @@ -759,9 +816,13 @@ SearchHistory loadSearchHistory() { return App.getDefaultGson().fromJson(json, SearchHistory.class); } - void saveSearchHistory(SearchHistory sh) { - final String json = App.getDefaultGson().toJson(sh); - Preferences.setString(Prefkey.searchHistory, json); + void saveSearchHistory(@Nullable SearchHistory sh) { + if (sh == null) { + Preferences.remove(Prefkey.searchHistory); + } else { + final String json = App.getDefaultGson().toJson(sh); + Preferences.setString(Prefkey.searchHistory, json); + } } // returns the modified SearchHistory @@ -790,12 +851,12 @@ boolean usingRevIndex() { } class SearchAdapter extends EasyAdapter { - IntArrayList searchResults; - String[] tokens; + final IntArrayList searchResults; + final SearchEngine.ReadyTokens rt; public SearchAdapter(IntArrayList searchResults, String[] tokens) { this.searchResults = searchResults; - this.tokens = tokens; + this.rt = tokens == null ? null : new SearchEngine.ReadyTokens(tokens); } @Override @@ -840,7 +901,7 @@ public int getCount() { final String verseText = U.removeSpecialCodes(searchInVersion.loadVerseText(ari)); if (verseText != null) { - lSnippet.setText(SearchEngine.hilite(verseText, tokens, checked? checkedTextColor: hiliteColor)); + lSnippet.setText(SearchEngine.hilite(verseText, rt, checked? checkedTextColor: hiliteColor)); } else { lSnippet.setText(R.string.generic_verse_not_available_in_this_version); } diff --git a/Alkitab/src/main/java/yuku/alkitab/base/ac/VersionsActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/ac/VersionsActivity.java index f31c2ddac..26ccd05f4 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/VersionsActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/VersionsActivity.java @@ -565,7 +565,8 @@ void handleFileOpenYes(String filename) { } mvDb.preset_name = preset_name; - S.getDb().insertVersionWithActive(mvDb, true); + S.getDb().insertOrUpdateVersionWithActive(mvDb, true); + MVersionDb.clearVersionImplCache(); App.getLbm().sendBroadcast(new Intent(VersionListFragment.ACTION_RELOAD)); } catch (Exception e) { diff --git a/Alkitab/src/main/java/yuku/alkitab/base/br/VersionDownloadCompleteReceiver.java b/Alkitab/src/main/java/yuku/alkitab/base/br/VersionDownloadCompleteReceiver.java index 3568fb2e8..d1808c3d5 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/br/VersionDownloadCompleteReceiver.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/br/VersionDownloadCompleteReceiver.java @@ -150,7 +150,8 @@ public void onReceive(Context context, Intent intent) { mvDb.modifyTime = modifyTime; mvDb.ordering = maxOrdering + 1; - S.getDb().insertVersionWithActive(mvDb, true); + S.getDb().insertOrUpdateVersionWithActive(mvDb, true); + MVersionDb.clearVersionImplCache(); Toast.makeText(App.context, TextUtils.expandTemplate(context.getText(R.string.version_download_complete), mvDb.longName), Toast.LENGTH_LONG).show(); diff --git a/Alkitab/src/main/java/yuku/alkitab/base/cp/Provider.java b/Alkitab/src/main/java/yuku/alkitab/base/cp/Provider.java index 84d2df9d2..59811db14 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/cp/Provider.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/cp/Provider.java @@ -270,10 +270,14 @@ private Cursor getCursorForRangeVerseAri(IntArrayList ariRanges, boolean formatt * @return number of verses put into the cursor */ private int resultForOneChapter(MatrixCursor cursor, Book book, int last_c, int ari_bc, int v_1_start, int v_1_end, boolean formatting) { + final SingleChapterVerses verses = S.activeVersion.loadChapterText(book, Ari.toChapter(ari_bc)); + if (verses == null) { + return 0; + } + int count = 0; - SingleChapterVerses verses = S.activeVersion.loadChapterText(book, Ari.toChapter(ari_bc)); for (int v_1 = v_1_start; v_1 <= v_1_end; v_1++) { - int v_0 = v_1 - 1; + final int v_0 = v_1 - 1; if (v_0 < verses.getVerseCount()) { int ari = ari_bc | v_1; String text = verses.getVerse(v_0); diff --git a/Alkitab/src/main/java/yuku/alkitab/base/fr/GotoGridFragment.java b/Alkitab/src/main/java/yuku/alkitab/base/fr/GotoGridFragment.java index 139189775..1ce245a35 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/fr/GotoGridFragment.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/fr/GotoGridFragment.java @@ -6,8 +6,6 @@ import android.support.v4.view.ViewCompat; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; -import android.text.SpannableStringBuilder; -import android.text.style.UnderlineSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -165,23 +163,17 @@ public static Bundle createArgs(int bookId, int chapter_1, int verse_1) { } protected void displaySelectedBookAndChapter() { - lSelectedBook.setText(underline(selectedBook.shortName)); + lSelectedBook.setText(selectedBook.shortName); lSelectedBook.setTextColor(U.getForegroundColorOnDarkBackgroundByBookId(selectedBook.bookId)); if (selectedChapter == 0) { lSelectedChapter.setVisibility(View.GONE); } else { lSelectedChapter.setVisibility(View.VISIBLE); ViewCompat.jumpDrawablesToCurrentState(lSelectedChapter); - lSelectedChapter.setText(underline("" + selectedChapter)); + lSelectedChapter.setText("" + selectedChapter); } } - private CharSequence underline(CharSequence cs) { - SpannableStringBuilder sb = SpannableStringBuilder.valueOf(cs); - sb.setSpan(new UnderlineSpan(), 0, cs.length(), 0); - return sb; - } - GridLayoutManager createLayoutManagerForNumbers() { return new GridLayoutManager(getActivity(), getResources().getInteger(R.integer.goto_grid_numeric_num_columns)); } diff --git a/Alkitab/src/main/java/yuku/alkitab/base/model/MVersionDb.java b/Alkitab/src/main/java/yuku/alkitab/base/model/MVersionDb.java index 959ab28bf..3af0f279f 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/model/MVersionDb.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/model/MVersionDb.java @@ -106,4 +106,8 @@ public boolean getActive() { final File f = new File(filename); return f.exists() && f.canRead(); } + + public static void clearVersionImplCache() { + impl_cache.clear(); + } } diff --git a/Alkitab/src/main/java/yuku/alkitab/base/model/VersionImpl.java b/Alkitab/src/main/java/yuku/alkitab/base/model/VersionImpl.java index 48bc53f83..3fb3af730 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/model/VersionImpl.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/model/VersionImpl.java @@ -250,10 +250,14 @@ public synchronized int loadVersesByAriRanges(IntArrayList ariRanges, IntArrayLi * @return number of verses put into the cursor */ private int resultForOneChapter(Book book, int ari_bc, int v_1_start, int v_1_end, IntArrayList result_aris, List result_verses) { + final SingleChapterVerses verses = loadChapterText(book, Ari.toChapter(ari_bc)); + if (verses == null) { + return 0; + } + int count = 0; - SingleChapterVerses verses = loadChapterText(book, Ari.toChapter(ari_bc)); for (int v_1 = v_1_start; v_1 <= v_1_end; v_1++) { - int v_0 = v_1 - 1; + final int v_0 = v_1 - 1; if (v_0 < verses.getVerseCount()) { final int ari = ari_bc | v_1; final String verseText = verses.getVerse(v_0); @@ -283,6 +287,7 @@ public synchronized int loadPericope(int bookId, int chapter_1, int[] aris, Peri } @Override + @Nullable public synchronized SingleChapterVerses loadChapterText(Book book, int chapter_1) { if (book == null) { return null; @@ -292,6 +297,7 @@ public synchronized SingleChapterVerses loadChapterText(Book book, int chapter_1 } @Override + @Nullable public synchronized SingleChapterVerses loadChapterTextLowercased(Book book, int chapter_1) { if (book == null) { return null; diff --git a/Alkitab/src/main/java/yuku/alkitab/base/storage/InternalDb.java b/Alkitab/src/main/java/yuku/alkitab/base/storage/InternalDb.java index ee2e1bacd..70171f227 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/storage/InternalDb.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/storage/InternalDb.java @@ -598,7 +598,7 @@ public List listAllVersions() { public void setVersionActive(MVersionDb mv, boolean active) { final SQLiteDatabase db = helper.getWritableDatabase(); final ContentValues cv = new ContentValues(); - cv.put(Db.Version.active, active? 1: 0); + cv.put(Db.Version.active, active ? 1 : 0); if (mv.preset_name != null) { db.update(Db.TABLE_Version, cv, Db.Version.preset_name + "=?", new String[] {mv.preset_name}); @@ -612,7 +612,13 @@ public int getVersionMaxOrdering() { return (int) DatabaseUtils.longForQuery(db, "select max(" + Db.Version.ordering + ") from " + Db.TABLE_Version, null); } - public void insertVersionWithActive(MVersionDb mv, boolean active) { + /** + * If the filename of the inserted mv already exists in the table, + * update is performed instead of an insert. + * In that case, the mv.ordering will be changed to the one in the table, + * and the passed-in mv.ordering will not be used. + */ + public void insertOrUpdateVersionWithActive(MVersionDb mv, boolean active) { final SQLiteDatabase db = helper.getWritableDatabase(); final ContentValues cv = new ContentValues(); cv.put(Db.Version.locale, mv.locale); @@ -627,11 +633,18 @@ public void insertVersionWithActive(MVersionDb mv, boolean active) { db.beginTransactionNonExclusive(); try { // prevent insert for the same filename (absolute path), update instead - final long count = DatabaseUtils.queryNumEntries(db, Db.TABLE_Version, Db.Version.filename + "=?", new String[]{mv.filename}); - if (count == 0) { - db.insert(Db.TABLE_Version, null, cv); - } else { - db.update(Db.TABLE_Version, cv, Db.Version.filename + "=?", new String[]{mv.filename}); + try (Cursor c = db.query(Db.TABLE_Version, Array("_id", Db.Version.ordering), Db.Version.filename + "=?", Array(mv.filename), null, null, null)) { + if (c.moveToNext()) { + final long _id = c.getLong(0); + final int ordering = c.getInt(1); + + mv.ordering = ordering; + cv.put(Db.Version.ordering, ordering); + + db.update(Db.TABLE_Version, cv, "_id=?", ToStringArray(_id)); + } else { + db.insert(Db.TABLE_Version, null, cv); + } } db.setTransactionSuccessful(); diff --git a/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java b/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java index f9efe6832..bd7a2cb07 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java @@ -123,7 +123,7 @@ public static String unescapeHighUnicode(String input) { } } - public static void importBookmarks(final Activity activity, @NonNull final InputStream fis, final boolean finishActivityAfterwards) { + public static void importBookmarks(final Activity activity, @NonNull final InputStream fis, final boolean finishActivityAfterwards, final Runnable runWhenDone) { final MaterialDialog pd = new MaterialDialog.Builder(activity) .content(R.string.mengimpor_titiktiga) .cancelable(false) @@ -229,6 +229,8 @@ protected void onPostExecute(@NonNull Object result) { dialog.setOnDismissListener(dialog1 -> activity.finish()); } } + + if (runWhenDone != null) runWhenDone.run(); } }.execute(); } @@ -238,34 +240,31 @@ public static void importBookmarks(List markers, TObjectIntHashMap markerRelIdToMarker = new TIntObjectHashMap<>(); { // write new markers (if not available yet) - for (final Marker marker : markers) { + for (int i = 0; i < markers.size(); i++) { + Marker marker = markers.get(i); final int marker_relId = markerToRelIdMap.get(marker); // migrate: look for existing marker with same kind, ari, and content - final Cursor cursor = db.query( + try (Cursor cursor = db.query( Db.TABLE_Marker, null, Db.Marker.ari + "=? and " + Db.Marker.kind + "=? and " + Db.Marker.caption + "=?", ToStringArray(marker.ari, marker.kind.code, marker.caption), null, null, null - ); - - // ------------------------------ get _id from - // exists: (nop) [1] - // !exists: insert [2] - final long _id; - if (cursor.moveToNext()) { - _id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); /* [1] */ - } else { - _id = InternalDb.insertMarker(db, marker); /* [2] */ - } - cursor.close(); + )) { + if (cursor.moveToNext()) { + marker = InternalDb.markerFromCursor(cursor); + markers.set(i, marker); + } else { + InternalDb.insertMarker(db, marker); + } - // map it - markerRelIdToAbsIdMap.put(marker_relId, _id); + // map it + markerRelIdToMarker.put(marker_relId, marker); + } } } @@ -273,18 +272,17 @@ public static void importBookmarks(List markers, TObjectIntHashMap 0) { + if (marker != null) { // existing labels > 0: ignore // existing labels == 0: insert - final int existing_label_count = (int) DatabaseUtils.queryNumEntries(db, Db.TABLE_Marker_Label, "_id=?", ToStringArray(marker_id)); + final int existing_label_count = (int) DatabaseUtils.queryNumEntries(db, Db.TABLE_Marker_Label, Db.Marker_Label.marker_gid + "=?", ToStringArray(marker.gid)); if (existing_label_count == 0) { for (int label_relId : label_relIds.toArray()) { final long label_id = labelRelIdToAbsIdMap.get(label_relId); if (label_id > 0) { - final Marker marker = S.getDb().getMarkerById(marker_id); final Label label = S.getDb().getLabelById(label_id); final Marker_Label marker_label = Marker_Label.createNewMarker_Label(marker.gid, label.gid); InternalDb.insertMarker_LabelIfNotExists(db, marker_label); @@ -294,7 +292,7 @@ public static void importBookmarks(List markers, TObjectIntHashMap raw_tokens = new ArrayList<>(); + final List raw_tokens = new ArrayList<>(); - Matcher matcher = QueryTokenizer.oneToken.matcher(query.toLowerCase(Locale.getDefault())); + final Matcher matcher = QueryTokenizer.oneToken.matcher(query.toLowerCase(Locale.getDefault())); while (matcher.find()) { raw_tokens.add(matcher.group(1) + matcher.group(2)); } - //# process raw tokens - List processed = new ArrayList<>(raw_tokens.size()); + // process raw tokens + final List processed = new ArrayList<>(raw_tokens.size()); for (String raw_token : raw_tokens) { - if (isPlussedToken(raw_token)) { - String tokenWithoutPlus = tokenWithoutPlus(raw_token); - if (tokenWithoutPlus.length() > 0) { - processed.add("+" + tokenWithoutPlus.replace("\"", "")); - } - } else { - if (raw_token.length() > 2 && raw_token.startsWith("\"") && raw_token.endsWith("\"")) { - processed.add("+" + raw_token.replace("\"", "")); + boolean plussed = false; + + while (true) { + if (raw_token.length() >= 1 && raw_token.charAt(0) == '+') { + // prefixed with '+' + plussed = true; + raw_token = raw_token.substring(1); + } else if (raw_token.length() >= 2 && raw_token.charAt(0) == '"' && raw_token.charAt(raw_token.length() - 1) == '"') { + // surrounded by quotes + plussed = true; + raw_token = raw_token.substring(1, raw_token.length() - 1); + } else if (raw_token.length() >= 2 && raw_token.charAt(0) == '"') { + // opening quote is present, but no closing quote. This is still considered as a complete quoted token. + plussed = true; + raw_token = raw_token.substring(1); } else { - processed.add(raw_token.replace("\"", "")); + break; } } + + if (raw_token.length() > 0) { + processed.add(plussed ? "+" + raw_token : raw_token); + } } return processed.toArray(new String[processed.size()]); } public static boolean isPlussedToken(String token) { - return (token.startsWith("+")); + return token.length() >= 1 && token.charAt(0) == '+'; } - public static String tokenWithoutPlus(String token) { - int pos = 0; - int len = token.length(); - while (true) { - if (pos >= len) break; - if (token.charAt(pos) == '+') { - pos++; - } else { - break; + /** + * Removes a single '+' from the token if exists. + * @param token may start or not start with '+' + */ + public static String tokenWithoutPlus(@NonNull String token) { + if (token.length() >= 1) { + if (token.charAt(0) == '+') { + return token.substring(1); } } - if (pos == 0) return token; - return token.substring(pos); + return token; } public static Matcher[] matcherizeTokens(String[] tokens) { @@ -89,31 +102,26 @@ public static Matcher[] matcherizeTokens(String[] tokens) { return res; } - static boolean isMultiwordToken(String token) { - int start = 0; - if (isPlussedToken(token)) { - start = 1; - } - for (int i = start, len = token.length(); i < len; i++) { - char c = token.charAt(i); - if (! (Character.isLetter(c) || ((c=='-' || c=='\'') && i > start && i < len-1))) { - return true; - } - } - return false; - } - - static Pattern pattern_letters = Pattern.compile("[\\p{javaLetter}'-]+"); + static Pattern pattern_letters = Pattern.compile("[\\p{javaLetterOrDigit}'-]+"); /** - * For tokens such as "abc.,- def", which will be re-tokenized to "abc" "def" + * For tokens such as "abc.,- def123", which will be re-tokenized to "abc" "def123" + * @return null if the token is not a multiword token (i.e. not an array with 1 element!). */ - static List tokenizeMultiwordToken(String token) { - List res = new ArrayList<>(); - Matcher m = pattern_letters.matcher(token); + @Nullable static String[] tokenizeMultiwordToken(String token) { + List res = null; + final Matcher m = pattern_letters.matcher(token); while (m.find()) { + if (res == null) { + res = new ArrayList<>(); + } res.add(m.group()); } - return res; + + if (res == null || res.size() <= 1) { + return null; + } + + return res.toArray(new String[res.size()]); } } \ No newline at end of file diff --git a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java index 96b15f95a..22a4a3bb9 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -3,6 +3,8 @@ import android.graphics.Typeface; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; @@ -27,10 +29,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.Semaphore; public class SearchEngine { @@ -39,11 +39,11 @@ public class SearchEngine { public static class Query implements Parcelable { public String query_string; public SparseBooleanArray bookIds; - + @Override public int describeContents() { return 0; } - + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(query_string); dest.writeSparseBooleanArray(bookIds); @@ -62,23 +62,60 @@ public static class Query implements Parcelable { } }; } - + static class RevIndex extends HashMap { public RevIndex() { super(32768); } } - + + /** + * Contains processed tokens that is more efficient to be passed in to methods here such as + * {@link #hilite(CharSequence, ReadyTokens, int)} and {@link #satisfiesTokens(String, ReadyTokens)}. + */ + public static class ReadyTokens { + final int token_count; + final boolean[] hasPlusses; + /** Already without plusses */ + final String[] tokens; + final String[][] multiwords_tokens; + + public ReadyTokens(final String[] tokens) { + final int token_count = tokens.length; + this.token_count = token_count; + this.hasPlusses = new boolean[token_count]; + this.tokens = new String[token_count]; + this.multiwords_tokens = new String[token_count][]; + + for (int i = 0; i < token_count; i++) { + final String token = tokens[i]; + if (QueryTokenizer.isPlussedToken(token)) { + this.hasPlusses[i] = true; + + final String tokenWithoutPlus = QueryTokenizer.tokenWithoutPlus(token); + this.tokens[i] = tokenWithoutPlus; + + final String[] multiword = QueryTokenizer.tokenizeMultiwordToken(tokenWithoutPlus); + if (multiword != null) { + this.multiwords_tokens[i] = multiword; + } + } else { + this.tokens[i] = token; + } + } + } + } + private static SoftReference cache_revIndex; private static Semaphore revIndexLoading = new Semaphore(1); - + public static IntArrayList searchByGrep(final Version version, final Query query) { - String[] words = QueryTokenizer.tokenize(query.query_string); - - // urutkan berdasarkan panjang, lalu abjad - Arrays.sort(words, (object1, object2) -> { - int len1 = object1.length(); - int len2 = object2.length(); + String[] tokens = QueryTokenizer.tokenize(query.query_string); + + // sort by word length, then alphabetically + Arrays.sort(tokens, (object1, object2) -> { + final int len1 = object1.length(); + final int len2 = object2.length(); if (len1 > len2) return -1; if (len1 == len2) { @@ -86,73 +123,61 @@ public static IntArrayList searchByGrep(final Version version, final Query query } return 1; }); - + // remove duplicates { - ArrayList awords = new ArrayList<>(); + final ArrayList atokens = new ArrayList<>(); String last = null; - for (String word: words) { - if (!word.equals(last)) { - awords.add(word); + for (String token: tokens) { + if (!token.equals(last)) { + atokens.add(token); } - last = word; + last = token; } - words = awords.toArray(new String[awords.size()]); - if (BuildConfig.DEBUG) Log.d(TAG, "words = " + Arrays.toString(words)); + tokens = atokens.toArray(new String[atokens.size()]); + if (BuildConfig.DEBUG) Log.d(TAG, "tokens = " + Arrays.toString(tokens)); } - + // really search IntArrayList result = null; - - { - int index = 0; - - while (true) { - if (index >= words.length) { - break; - } - - String word = words[index]; - - IntArrayList prev = result; - - { - long ms = System.currentTimeMillis(); - result = searchByGrepInside(version, word, prev, query.bookIds); - Log.d(TAG, "search word '" + word + "' needed: " + (System.currentTimeMillis() - ms) + " ms"); - } - - if (prev != null) { - Log.d(TAG, "Will intersect " + prev.size() + " elements with " + result.size() + " elements..."); - result = intersect(prev, result); - Log.d(TAG, "... the result is " + result.size() + " elements"); - } - - index++; + + for (final String token : tokens) { + final IntArrayList prev = result; + + { + long ms = System.currentTimeMillis(); + result = searchByGrepInside(version, token, prev, query.bookIds); + Log.d(TAG, "search token '" + token + "' needed: " + (System.currentTimeMillis() - ms) + " ms"); + } + + if (prev != null) { + Log.d(TAG, "Will intersect " + prev.size() + " elements with " + result.size() + " elements..."); + result = intersect(prev, result); + Log.d(TAG, "... the result is " + result.size() + " elements"); } } - + return result; } private static IntArrayList intersect(IntArrayList a, IntArrayList b) { IntArrayList res = new IntArrayList(a.size()); - + int[] aa = a.buffer(); int[] bb = b.buffer(); int alen = a.size(); int blen = b.size(); - + int apos = 0; int bpos = 0; - + while (true) { if (apos >= alen) break; if (bpos >= blen) break; - + int av = aa[apos]; int bv = bb[bpos]; - + if (av == bv) { res.add(av); apos++; @@ -163,7 +188,7 @@ private static IntArrayList intersect(IntArrayList a, IntArrayList b) { apos++; } } - + return res; } @@ -175,13 +200,13 @@ private static int nextAri(IntArrayList source, int[] ppos, int lastAriBc) { int[] s = source.buffer(); int len = source.size(); int pos = ppos[0]; - + while (true) { if (pos >= len) return 0x0; - + int curAri = s[pos]; int curAriBc = Ari.toBookChapter(curAri); - + if (curAriBc != lastAriBc) { // found! pos++; @@ -194,105 +219,134 @@ private static int nextAri(IntArrayList source, int[] ppos, int lastAriBc) { } } - static IntArrayList searchByGrepInside(final Version version, String word, final IntArrayList source, final SparseBooleanArray bookIds) { + static IntArrayList searchByGrepInside(final Version version, String token, final IntArrayList source, final SparseBooleanArray bookIds) { final IntArrayList res = new IntArrayList(); - boolean hasPlus = false; - - if (QueryTokenizer.isPlussedToken(word)) { - hasPlus = true; - word = QueryTokenizer.tokenWithoutPlus(word); + final boolean hasPlus = QueryTokenizer.isPlussedToken(token); + + if (hasPlus) { + token = QueryTokenizer.tokenWithoutPlus(token); } - + if (source == null) { for (Book book: version.getConsecutiveBooks()) { if (!bookIds.get(book.bookId, false)) { continue; // the book is not included in selected books to be searched } - - int chapter_count = book.chapter_count; - - for (int chapter_1 = 1; chapter_1 <= chapter_count; chapter_1++) { + + for (int chapter_1 = 1; chapter_1 <= book.chapter_count; chapter_1++) { // try to find it wholly in a chapter - String oneChapter = version.loadChapterTextLowercasedWithoutSplit(book, chapter_1); - if (oneChapter != null && oneChapter.contains(word)) { - // only do the following when inside a chapter, word is found - searchByGrepInChapter(oneChapter, word, res, Ari.encode(book.bookId, chapter_1, 0), hasPlus); - } + final int ariBc = Ari.encode(book.bookId, chapter_1, 0); + searchByGrepForOneChapter(version, book, chapter_1, token, hasPlus, ariBc, res); } - + if (BuildConfig.DEBUG) Log.d(TAG, "searchByGrepInside book " + book.shortName + " done. res.size = " + res.size()); } } else { // search only on book-chapters that are in the source int count = 0; // for stats - + int[] ppos = new int[1]; int curAriBc = 0x000000; - + while (true) { curAriBc = nextAri(source, ppos, curAriBc); - if (curAriBc == 0) break; // habis - + if (curAriBc == 0) break; // no more + // No need to check null book, because we go here only after searching a previous token which is based on - // getConsecutiveBooks which is impossible to have null books. + // getConsecutiveBooks, which is impossible to have null books. final Book book = version.getBook(Ari.toBook(curAriBc)); - int chapter_1 = Ari.toChapter(curAriBc); - - final String oneChapter = version.loadChapterTextLowercasedWithoutSplit(book, chapter_1); - if (oneChapter.contains(word)) { - // Only to the following if inside a chapter we find the word. - searchByGrepInChapter(oneChapter, word, res, curAriBc, hasPlus); - } - + final int chapter_1 = Ari.toChapter(curAriBc); + + searchByGrepForOneChapter(version, book, chapter_1, token, hasPlus, curAriBc, res); + count++; } - + if (BuildConfig.DEBUG) Log.d(TAG, "searchByGrepInside book with source " + source.size() + " needed to read as many as " + count + " book-chapter. res.size=" + res.size()); } - + return res; } - private static void searchByGrepInChapter(String oneChapter, String word, IntArrayList res, int base, boolean hasPlus) { + /** + * @param token searched token without plusses + * @param res (output) result aris + * @param ariBc book-chapter ari, with verse must be set to 0 + * @param hasPlus whether the token had plus + */ + private static void searchByGrepForOneChapter(final Version version, final Book book, final int chapter_1, final String token, final boolean hasPlus, final int ariBc, final IntArrayList res) { + // This is a string of one chapter with verses joined by 0x0a ('\n') + final String oneChapter = version.loadChapterTextLowercasedWithoutSplit(book, chapter_1); + if (oneChapter == null) { + return; + } + int verse_0 = 0; int lastV = -1; - - int posWord; + + // Initial search + String[] multiword = null; + final int[] consumedLengthPtr = {0}; + + // posToken is the last found position of the searched token + // consumedLength is how much characters in the oneChapter was consumed when searching for the token. + // Both of these variables must be set together in all cases. + int posToken; + int consumedLength; + if (hasPlus) { - posWord = indexOfWholeWord(oneChapter, word, 0); - if (posWord == -1) { - return; + multiword = QueryTokenizer.tokenizeMultiwordToken(token); + + if (multiword != null) { + posToken = indexOfWholeMultiword(oneChapter, multiword, 0, true, consumedLengthPtr); + consumedLength = consumedLengthPtr[0]; + } else { + posToken = indexOfWholeWord(oneChapter, token, 0); + consumedLength = token.length(); } } else { - posWord = oneChapter.indexOf(word); + posToken = oneChapter.indexOf(token, 0); + consumedLength = token.length(); } - + + if (posToken == -1) { + // initial search does not return results. It means the whole chapter does not contain the token. + return; + } + int posN = oneChapter.indexOf(0x0a); - + while (true) { - if (posN < posWord) { + if (posN < posToken) { verse_0++; - posN = oneChapter.indexOf(0x0a, posN+1); + posN = oneChapter.indexOf(0x0a, posN + 1); if (posN == -1) { return; } } else { if (verse_0 != lastV) { - res.add(base + verse_0 + 1); // +1 to make it verse_1 + res.add(ariBc + verse_0 + 1); // +1 to make it verse_1 lastV = verse_0; } if (hasPlus) { - posWord = indexOfWholeWord(oneChapter, word, posWord+1); + if (multiword != null) { + posToken = indexOfWholeMultiword(oneChapter, multiword, posToken + consumedLength, true, consumedLengthPtr); + consumedLength = consumedLengthPtr[0]; + } else { + posToken = indexOfWholeWord(oneChapter, token, posToken + consumedLength); + consumedLength = token.length(); + } } else { - posWord = oneChapter.indexOf(word, posWord+1); + posToken = oneChapter.indexOf(token, posToken + consumedLength); + consumedLength = token.length(); } - if (posWord == -1) { + if (posToken == -1) { return; } } } } - + public static IntArrayList searchByRevIndex(final Version version, final Query query) { TimingLogger timing = new TimingLogger("RevIndex", "searchByRevIndex"); RevIndex revIndex; @@ -307,61 +361,27 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q revIndexLoading.release(); } timing.addSplit("Load rev index"); - + boolean[] passBitmapOr = new boolean[32768]; boolean[] passBitmapAnd = new boolean[32768]; Arrays.fill(passBitmapAnd, true); - - // Query e.g.: "a b" c +"d e" +f - List tokens; // this will be: "a" "b" "c" "+d" "+e" "+f" - List multiwords = null; // this will be: "a b" "+d e" - { - Set tokenSet = new LinkedHashSet<>(Arrays.asList(QueryTokenizer.tokenize(query.query_string))); - Log.d(TAG, "Tokens before retokenization:"); - for (String token: tokenSet) { + + final ReadyTokens rt = new ReadyTokens(QueryTokenizer.tokenize(query.query_string)); + + if (BuildConfig.DEBUG) { + Log.d(TAG, "Tokens after retokenization:"); + for (String token: rt.tokens) { Log.d(TAG, "- token: " + token); } - - Set tokenSet2 = new LinkedHashSet<>(); - for (String token: tokenSet) { - if (QueryTokenizer.isMultiwordToken(token)) { - if (multiwords == null) { - multiwords = new ArrayList<>(); - } - multiwords.add(token); - boolean token_plussed = QueryTokenizer.isPlussedToken(token); - String token_bare = QueryTokenizer.tokenWithoutPlus(token); - for (String token2: QueryTokenizer.tokenizeMultiwordToken(token_bare)) { - if (token_plussed) { - tokenSet2.add("+" + token2); - } else { - tokenSet2.add(token2); - } - } - } else { - tokenSet2.add(token); - } - } - - if (BuildConfig.DEBUG) { - Log.d(TAG, "Tokens after retokenization:"); - for (String token: tokenSet2) { - Log.d(TAG, "- token: " + token); - } - - if (multiwords != null) { - Log.d(TAG, "Multiwords:"); - for (String multiword: multiwords) { - Log.d(TAG, "- multiword: " + multiword); - } - } + + Log.d(TAG, "Multiwords:"); + for (String[] multiword: rt.multiwords_tokens) { + Log.d(TAG, "- multiword: " + Arrays.toString(multiword)); } - - tokens = new ArrayList<>(tokenSet2); } - + timing.addSplit("Tokenize query"); - + // optimization, if user doesn't filter any books boolean wholeBibleSearched = true; boolean[] searchedBookIds = new boolean[66]; @@ -375,14 +395,19 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } } } - - for (String token: tokens) { - boolean plussed = QueryTokenizer.isPlussedToken(token); - String token_bare = QueryTokenizer.tokenWithoutPlus(token); - + + for (int i = 0; i < rt.token_count; i++) { + if (rt.multiwords_tokens[i] != null) { + // This is multiword token, handled separately below + continue; + } + + final String token_bare = rt.tokens[i]; + final boolean plussed = rt.hasPlusses[i]; + Arrays.fill(passBitmapOr, false); - - for (Map.Entry e: revIndex.entrySet()) { + + for (Map.Entry e : revIndex.entrySet()) { String word = e.getKey(); boolean match = false; @@ -391,24 +416,24 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } else { if (word.contains(token_bare)) match = true; } - + if (match) { int[] lids = e.getValue(); - for (int lid: lids) { + for (int lid : lids) { passBitmapOr[lid] = true; // OR operation } } } - + int c = 0; - for (boolean b: passBitmapOr) { + for (boolean b : passBitmapOr) { if (b) c++; } - timing.addSplit("gather lid for token '" + token + "' (" + c + ")"); - + timing.addSplit("gather lid for token '" + token_bare + "' (" + c + ")"); + // AND operation with existing word(s) - for (int i = passBitmapOr.length - 1; i >= 0; i--) { - passBitmapAnd[i] &= passBitmapOr[i]; + for (int j = passBitmapOr.length - 1; j >= 0; j--) { + passBitmapAnd[j] &= passBitmapOr[j]; } timing.addSplit("AND operation"); } @@ -430,46 +455,48 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } } timing.addSplit("convert matching lids to aris (" + res.size() + ")"); - + // last check: whether multiword tokens are all matching. No way to find this except by loading the text // and examining one by one whether the text contains those multiword tokens - if (multiwords != null) { - IntArrayList res2 = new IntArrayList(res.size()); - - // separate the pluses - String[] multiwords_bare = new String[multiwords.size()]; - boolean[] multiwords_plussed = new boolean[multiwords.size()]; - - for (int i = 0, len = multiwords.size(); i < len; i++) { - String multiword = multiwords.get(i); - multiwords_bare[i] = QueryTokenizer.tokenWithoutPlus(multiword); - multiwords_plussed[i] = QueryTokenizer.isPlussedToken(multiword); - } - + final List multiwords = new ArrayList<>(); + for (final String[] multiword_tokens : rt.multiwords_tokens) { + if (multiword_tokens != null) { + multiwords.add(multiword_tokens); + } + } + + if (multiwords.size() > 0) { + final IntArrayList res2 = new IntArrayList(res.size()); + + final int[] consumedLengthPtr = {0}; + SingleChapterVerses loadedChapter = null; // the currently loaded chapter, to prevent repeated loading of same chapter int loadedAriCv = 0; // chapter and verse of current Ari for (int i = 0, len = res.size(); i < len; i++) { - int ari = res.get(i); - - int ariCv = Ari.toBookChapter(ari); + final int ari = res.get(i); + + final int ariCv = Ari.toBookChapter(ari); if (ariCv != loadedAriCv) { // we can't reuse, we need to load from disk - Book book = version.getBook(Ari.toBook(ari)); - if (book != null) { + final Book book = version.getBook(Ari.toBook(ari)); + if (book == null) { + continue; + } else { loadedChapter = version.loadChapterTextLowercased(book, Ari.toChapter(ari)); loadedAriCv = ariCv; } } - - int verse_1 = Ari.toVerse(ari); + + if (loadedChapter == null) { + continue; + } + + final int verse_1 = Ari.toVerse(ari); if (verse_1 >= 1 && verse_1 <= loadedChapter.getVerseCount()) { - String text = loadedChapter.getVerse(verse_1 - 1); + final String text = loadedChapter.getVerse(verse_1 - 1); if (text != null) { boolean passed = true; - for (int j = 0, len2 = multiwords_bare.length; j < len2; j++) { - String multiword_bare = multiwords_bare[j]; - boolean multiword_plussed = multiwords_plussed[j]; - - if ((multiword_plussed && indexOfWholeWord(text, multiword_bare, 0) < 0) || (!multiword_plussed && !text.contains(multiword_bare))) { + for (final String[] multiword_tokens : multiwords) { + if (indexOfWholeMultiword(text, multiword_tokens, 0, false, consumedLengthPtr) == -1) { passed = false; break; } @@ -480,9 +507,9 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } } } - + res = res2; - + timing.addSplit("filter for multiword tokens (" + res.size() + ")"); } @@ -490,8 +517,8 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q return res; } - - @SuppressWarnings("synthetic-access") public static void preloadRevIndex() { + + public static void preloadRevIndex() { new Thread() { @Override public void run() { TimingLogger timing = new TimingLogger("RevIndex", "preloadRevIndex"); @@ -506,11 +533,11 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } }.start(); } - + /** * Revindex: an index used for searching quickly. * The index is keyed on the word for searching, and the value is the list of verses' lid (KJV verse number, 1..31102). - * + * * Format of the Revindex file: * int total_word_count * { @@ -522,14 +549,14 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q * byte[] verse_list // see below * }[word_by_len_count] * }[] // until total_word_count is taken - * + * * The verses in verse_list are stored in either 8bit or 16bit, depending on the difference to the last entry before the current entry. * The first entry on the list is always 16 bit. * If one verse is specified in 16 bits, the 15-bit LSB is the verse lid itself (max 32767, although 31102 is the real max) * in binary: 1xxxxxxx xxxxxxxx where x is the absolute verse lid as 15 bit uint. * If one verse is specified in 8 bits, the 7-bit LSB is the difference between this verse and the last verse. * in binary: 0ddddddd where d is the relative verse lid as 7 bit uint. - * For example, if a word is located at lids [0xff, 0x100, 0x300, 0x305], the stored data in the disk will be + * For example, if a word is located at lids [0xff, 0x100, 0x300, 0x305], the stored data in the disk will be * in bytes: 0x80, 0xff, 0x01, 0x83, 0x00, 0x05. */ private static RevIndex loadRevIndex() { @@ -550,22 +577,22 @@ private static RevIndex loadRevIndex() { final RevIndex res = new RevIndex(); final InputStream raw = new BufferedInputStream(assetInputStream, 65536); - + byte[] buf = new byte[256]; try { BintexReader br = new BintexReader(raw); - + int total_word_count = br.readInt(); int word_count = 0; - + while (true) { int word_len = br.readUint8(); int word_by_len_count = br.readInt(); - + for (int i = 0; i < word_by_len_count; i++) { br.readRaw(buf, 0, word_len); @SuppressWarnings("deprecation") String word = new String(buf, 0, 0, word_len); - + int lid_count = br.readUint16(); int last_lid = 0; int[] lids = new int[lid_count]; @@ -582,10 +609,10 @@ private static RevIndex loadRevIndex() { last_lid = lid; lids[pos++] = lid; } - + res.put(word, lids); } - + word_count += word_by_len_count; if (word_count >= total_word_count) { break; @@ -593,96 +620,185 @@ private static RevIndex loadRevIndex() { } br.close(); - } catch (IOException e) { + } catch (IOException e) { return null; } - + cache_revIndex = new SoftReference<>(res); return res; } /** - * Case sensitive! Make sure s and words have been lowercased (or normalized). + * Case sensitive! Make sure s and rt tokens have been lowercased (or normalized). */ - public static boolean satisfiesQuery(String s, String[] words) { - for (String word: words) { - boolean hasPlus = false; - - if (QueryTokenizer.isPlussedToken(word)) { - hasPlus = true; - word = QueryTokenizer.tokenWithoutPlus(word); - } - - int wordPos; + public static boolean satisfiesTokens(final String s, @NonNull final ReadyTokens rt) { + for (int i = 0; i < rt.token_count; i++) { + final boolean hasPlus = rt.hasPlusses[i]; + + final int posToken; if (hasPlus) { - wordPos = indexOfWholeWord(s, word, 0); + final String[] multiword_tokens = rt.multiwords_tokens[i]; + if (multiword_tokens != null) { + posToken = indexOfWholeMultiword(s, multiword_tokens, 0, false, null); + } else { + posToken = indexOfWholeWord(s, rt.tokens[i], 0); + } } else { - wordPos = s.indexOf(word); + posToken = s.indexOf(rt.tokens[i]); } - if (wordPos == -1) { + + if (posToken == -1) { return false; } } return true; } + /** + * This looks for a word that is surrounded by non-letter-or-digit characters. + * This works well only if the word is not a multiword. + * @param text haystack + * @param word needle + * @param start start at character + * @return -1 or position of the word + */ private static int indexOfWholeWord(String text, String word, int start) { - int len = text.length(); - + final int len = text.length(); + while (true) { - int pos = text.indexOf(word, start); + final int pos = text.indexOf(word, start); if (pos == -1) return -1; - - // pos bukan -1 - - // cek kiri - // [pos] [charat pos-1] - // 0 * ok - // >0 alpha ng - // >0 !alpha ok - if (pos != 0 && Character.isLetter(text.charAt(pos - 1))) { - start = pos + 1; - continue; + + // check left + // [pos] [charat pos-1] [charat pos-2] + // 0 ok + // >1 alnum '@' ok + // >1 alnum not '@' ng + // >0 alnum ng + // >0 not alnum ok + if (pos != 0 && Character.isLetterOrDigit(text.charAt(pos - 1))) { + if (pos != 1 && text.charAt(pos - 2) == '@') { + // oh, before this word there is a tag. Then it is OK. + } else { + start = pos + 1; // give up + continue; + } } - - // cek kanan + + // check right int end = pos + word.length(); - // [end] [charat end] - // len * ok - // != len alpha ng - // != len !alpha ok - if (end != len && Character.isLetter(text.charAt(end))) { - start = pos + 1; + // [end] [charat end] + // len * ok + // != len alnum ng + // != len not alnum ok + if (end != len && Character.isLetterOrDigit(text.charAt(end))) { + start = pos + 1; // give up continue; } - - // lulus + + // passed return pos; } } - public static SpannableStringBuilder hilite(final CharSequence s, String[] words, int hiliteColor) { - final SpannableStringBuilder res = new SpannableStringBuilder(s); - - if (words == null) { - return res; - } - - int word_count = words.length; - boolean[] hasPlusses = new boolean[word_count]; - { // point to copy - String[] words2 = new String[word_count]; - System.arraycopy(words, 0, words2, 0, word_count); - for (int i = 0; i < word_count; i++) { - if (QueryTokenizer.isPlussedToken(words2[i])) { - words2[i] = QueryTokenizer.tokenWithoutPlus(words2[i]); - hasPlusses[i] = true; + /** + * This looks for a multiword that is surrounded by non-letter characters. + * This works for multiword because it tries to strip tags and punctuations from the text before matching. + * @param text haystack. + * @param multiword multiword that has been split into words. Must have at least one element. + * @param start character index of text to start searching from + * @param isNewlineDelimitedText text has '\n' as delimiter between verses. multiword cannot be searched across different verses. + * @param consumedLengthPtr (length-1 array output) how many characters matched from the source text to satisfy the multiword. Will be 0 if this method returns -1. + * @return -1 or position of the multiword. + */ + private static int indexOfWholeMultiword(String text, String[] multiword, int start, boolean isNewlineDelimitedText, @Nullable int[] consumedLengthPtr) { + final int len = text.length(); + final String firstWord = multiword[0]; + + findAllWords: while (true) { + final int firstPos = indexOfWholeWord(text, firstWord, start); + if (firstPos == -1) { + // not even the first word is found, so we give up + if (consumedLengthPtr != null) consumedLengthPtr[0] = 0; + return -1; + } + + int pos = firstPos + firstWord.length(); + + // find the next words, but we want to consume everything after the previous word that is + // not eligible as search characters, which are tags and non-letters. + for (int i = 1, multiwordLen = multiword.length; i < multiwordLen; i++) { + final int posBeforeConsume = pos; + // consume! + while (pos < len) { + final char c = text.charAt(pos); + if (c == '@') { + if (pos == len - 1) { + // bad data (nothing after '@') + } else { + pos++; + final char d = text.charAt(pos); + if (d == '<') { + final int closingTagStart = text.indexOf("@>", pos + 1); + if (closingTagStart == -1) { + // bad data (no closing tag) + } else { + pos = closingTagStart + 1; + } + } else { + // single-letter formatting code, move on... + } + } + } else if (Character.isLetterOrDigit(c)) { + break; + } else if (isNewlineDelimitedText && c == '\n') { + // can't cross verse boundary, so we give up and try from beginning again + start = pos + 1; + continue findAllWords; + } else { + // non-letter, move on... + } + + pos++; + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "========================="); + Log.d(TAG, "multiword: " + Arrays.toString(multiword)); + Log.d(TAG, "text : #" + text.substring(Math.max(0, posBeforeConsume - multiword[i - 1].length()), Math.min(len, posBeforeConsume + 80)) + "#"); + Log.d(TAG, "skipped : #" + text.substring(posBeforeConsume, pos) + "#"); + Log.d(TAG, "=========================////"); } + + final String word = multiword[i]; + + final int foundWordStart = indexOfWholeWord(text, word, pos); + if (foundWordStart == -1 /* Not found... */ || foundWordStart != pos /* ...or another word comes */) { + // subsequent words is not found at the correct position, so loop from beginning again + start = pos; + continue findAllWords; + } + + // prepare for next iteration + pos = foundWordStart + word.length(); } - words = words2; + + // all words are found! + if (consumedLengthPtr != null) consumedLengthPtr[0] = pos - firstPos; + return firstPos; + } + } + + public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTokens rt, int hiliteColor) { + final SpannableStringBuilder res = new SpannableStringBuilder(s); + + if (rt == null) { + return res; } - // produce a plain text lowercased + final int token_count = rt.token_count; + + // from source text, produce a plain text lowercased final char[] newString = new char[s.length()]; for (int i = 0, len = s.length(); i < len; i++) { final char c = s.charAt(i); @@ -693,42 +809,57 @@ public static SpannableStringBuilder hilite(final CharSequence s, String[] words } } final String plainText = new String(newString); - + int pos = 0; - int[] attempt = new int[word_count]; - + final int[] attempts = new int[token_count]; + final int[] consumedLengths = new int[token_count]; + + // local vars for optimizations + final boolean[] hasPlusses = rt.hasPlusses; + final String[] tokens = rt.tokens; + final String[][] multiwords_tokens = rt.multiwords_tokens; + + // temp buf + final int[] consumedLengthPtr = {0}; while (true) { - for (int i = 0; i < word_count; i++) { + for (int i = 0; i < token_count; i++) { if (hasPlusses[i]) { - attempt[i] = indexOfWholeWord(plainText, words[i], pos); + if (multiwords_tokens[i] != null) { + attempts[i] = indexOfWholeMultiword(plainText, multiwords_tokens[i], pos, false, consumedLengthPtr); + consumedLengths[i] = consumedLengthPtr[0]; + } else { + attempts[i] = indexOfWholeWord(plainText, tokens[i], pos); + consumedLengths[i] = tokens[i].length(); + } } else { - attempt[i] = plainText.indexOf(words[i], pos); + attempts[i] = plainText.indexOf(tokens[i], pos); + consumedLengths[i] = tokens[i].length(); } } - + + // from the attempts above, find the earliest int minpos = Integer.MAX_VALUE; - int minword = -1; - - for (int i = 0; i < word_count; i++) { - if (attempt[i] >= 0) { // not -1 which means not found - if (attempt[i] < minpos) { - minpos = attempt[i]; - minword = i; + int mintokenindex = -1; + + for (int i = 0; i < token_count; i++) { + if (attempts[i] >= 0) { // not -1 which means not found + if (attempts[i] < minpos) { + minpos = attempts[i]; + mintokenindex = i; } } } - - if (minword == -1) { + + if (mintokenindex == -1) { break; // no more } - - pos = minpos + words[minword].length(); - - int topos = minpos + words[minword].length(); + + final int topos = minpos + consumedLengths[mintokenindex]; res.setSpan(new StyleSpan(Typeface.BOLD), minpos, topos, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); res.setSpan(new ForegroundColorSpan(hiliteColor), minpos, topos, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + pos = topos; } - + return res; } } diff --git a/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java b/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java index d5b8482cf..06d6d703d 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java @@ -8,62 +8,66 @@ import android.graphics.Typeface; import android.support.v4.view.MotionEventCompat; import android.util.AttributeSet; -import android.util.SparseArray; +import android.util.Log; import android.view.MotionEvent; import android.view.View; import yuku.alkitab.base.App; +import yuku.alkitab.debug.BuildConfig; import yuku.alkitab.debug.R; public class AttributeView extends View { + static final String TAG = AttributeView.class.getSimpleName(); public static final int PROGRESS_MARK_BITS_START = 8; public static final int PROGRESS_MARK_TOTAL_COUNT = 5; public static final int PROGRESS_MARK_BIT_MASK = (1 << PROGRESS_MARK_BITS_START) * ((1 << PROGRESS_MARK_TOTAL_COUNT) - 1); - - static Bitmap bookmarkBitmap = null; - static Bitmap noteBitmap = null; - static Bitmap[] progressMarkBitmap = new Bitmap[PROGRESS_MARK_TOTAL_COUNT]; - static Bitmap hasMapsBitmap = null; - static Paint alphaPaint = new Paint(); - static Paint attributeCountPaintBookmark = new Paint(); - static Paint attributeCountPaintNote; - - static { - attributeCountPaintBookmark.setTypeface(Typeface.DEFAULT_BOLD); - attributeCountPaintBookmark.setColor(0xff000000); - attributeCountPaintBookmark.setTextSize(App.context.getResources().getDisplayMetrics().density * 12.f); - attributeCountPaintBookmark.setAntiAlias(true); - attributeCountPaintBookmark.setTextAlign(Paint.Align.CENTER); - - attributeCountPaintNote = new Paint(attributeCountPaintBookmark); + private static final float COUNT_TEXT_SIZE_DP = 12.f; + + static Bitmap originalBookmarkBitmap = null; + static Bitmap scaledBookmarkBitmap = null; + static Bitmap originalNoteBitmap = null; + static Bitmap scaledNoteBitmap = null; + static Bitmap[] originalProgressMarkBitmaps = new Bitmap[PROGRESS_MARK_TOTAL_COUNT]; + static Bitmap[] scaledProgressMarkBitmaps = new Bitmap[PROGRESS_MARK_TOTAL_COUNT]; + static Bitmap originalHasMapsBitmap = null; + static Bitmap scaledHasMapsBitmap = null; + static Paint bookmarkCountPaint; + static Paint noteCountPaint; + + static float density = App.context.getResources().getDisplayMetrics().density; + + static { + bookmarkCountPaint = new Paint(); + bookmarkCountPaint.setTypeface(Typeface.DEFAULT_BOLD); + bookmarkCountPaint.setColor(0xff000000); + bookmarkCountPaint.setAntiAlias(true); + bookmarkCountPaint.setTextAlign(Paint.Align.CENTER); + + noteCountPaint = new Paint(bookmarkCountPaint); + noteCountPaint.setShadowLayer(density * 4, 0, 0, 0xffffffff); } int bookmark_count; int note_count; int progress_mark_bits; boolean has_maps; + float scale = 1.f; private VersesView.AttributeListener attributeListener; private int ari; - private static SparseArray progressMarkAnimationStartTimes = new SparseArray<>(); - private int drawOffsetLeft; - public AttributeView(final Context context) { super(context); - init(); } public AttributeView(final Context context, final AttributeSet attrs) { super(context, attrs); - init(); } - private void init() { - float density = getResources().getDisplayMetrics().density; - - drawOffsetLeft = Math.round(1 * density); - attributeCountPaintNote.setShadowLayer(density * 4, 0, 0, 0xffffffff); + public void setScale(final float scale) { + this.scale = scale; + requestLayout(); + invalidate(); } public int getBookmarkCount() { @@ -107,35 +111,68 @@ public void setHasMaps(final boolean has_maps) { } public boolean isShowingSomething() { - return bookmark_count > 0 || note_count > 0 || ((progress_mark_bits & PROGRESS_MARK_BIT_MASK) != 0); + return bookmark_count > 0 || note_count > 0 || (progress_mark_bits & PROGRESS_MARK_BIT_MASK) != 0 || has_maps; } - Bitmap getBookmarkBitmap() { - if (bookmarkBitmap == null) { - bookmarkBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_bookmark); + static Bitmap scale(Bitmap original, float scale) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "@@scale Scale needed. Called with scale=" + scale); } - return bookmarkBitmap; + + if (scale == 1.f) { + return Bitmap.createBitmap(original); + } + + final boolean filter = !(scale == 2.f || scale == 3.f || scale == 4.f); + return Bitmap.createScaledBitmap(original, Math.round(original.getWidth() * scale), Math.round(original.getHeight() * scale), filter); } - Bitmap getNoteBitmap() { - if (noteBitmap == null) { - noteBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_note); + Bitmap getScaledBookmarkBitmap() { + if (originalBookmarkBitmap == null) { + originalBookmarkBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_bookmark); } - return noteBitmap; + + if (scaledBookmarkBitmap == null || scaledBookmarkBitmap.getWidth() != Math.round(originalBookmarkBitmap.getWidth() * scale)) { + scaledBookmarkBitmap = scale(originalBookmarkBitmap, scale); + } + + return scaledBookmarkBitmap; + } + + Bitmap getScaledNoteBitmap() { + if (originalNoteBitmap == null) { + originalNoteBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_note); + } + + if (scaledNoteBitmap == null || scaledNoteBitmap.getWidth() != Math.round(originalNoteBitmap.getWidth() * scale)) { + scaledNoteBitmap = scale(originalNoteBitmap, scale); + } + + return scaledNoteBitmap; } - Bitmap getProgressMarkBitmapByPresetId(int preset_id) { - if (progressMarkBitmap[preset_id] == null) { - progressMarkBitmap[preset_id] = BitmapFactory.decodeResource(getResources(), getProgressMarkIconResource(preset_id)); + Bitmap getScaledProgressMarkBitmapByPresetId(int preset_id) { + if (originalProgressMarkBitmaps[preset_id] == null) { + originalProgressMarkBitmaps[preset_id] = BitmapFactory.decodeResource(getResources(), getProgressMarkIconResource(preset_id)); } - return progressMarkBitmap[preset_id]; + + if (scaledProgressMarkBitmaps[preset_id] == null || scaledProgressMarkBitmaps[preset_id].getWidth() != Math.round(originalProgressMarkBitmaps[preset_id].getWidth() * scale)) { + scaledProgressMarkBitmaps[preset_id] = scale(originalProgressMarkBitmaps[preset_id], scale); + } + + return scaledProgressMarkBitmaps[preset_id]; } - Bitmap getHasMapsBitmap() { - if (hasMapsBitmap == null) { - hasMapsBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_has_maps); + Bitmap getScaledHasMapsBitmap() { + if (originalHasMapsBitmap == null) { + originalHasMapsBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_has_maps); + } + + if (scaledHasMapsBitmap == null || scaledHasMapsBitmap.getWidth() != Math.round(originalHasMapsBitmap.getWidth() * scale)) { + scaledHasMapsBitmap = scale(originalHasMapsBitmap, scale); } - return hasMapsBitmap; + + return scaledHasMapsBitmap; } @Override @@ -143,36 +180,28 @@ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec int totalHeight = 0; int totalWidth = 0; if (bookmark_count > 0) { - final Bitmap bookmarkBitmap = getBookmarkBitmap(); - totalHeight += bookmarkBitmap.getHeight(); - if (totalWidth < bookmarkBitmap.getWidth()) { - totalWidth = bookmarkBitmap.getWidth(); - } + final Bitmap b = getScaledBookmarkBitmap(); + totalHeight += b.getHeight(); + totalWidth = Math.max(totalWidth, b.getWidth()); } if (note_count > 0) { - final Bitmap noteBitmap = getNoteBitmap(); - totalHeight += noteBitmap.getHeight(); - if (totalWidth < noteBitmap.getWidth()) { - totalWidth = noteBitmap.getWidth(); - } + final Bitmap b = getScaledNoteBitmap(); + totalHeight += b.getHeight(); + totalWidth = Math.max(totalWidth, b.getWidth()); } if (progress_mark_bits != 0) { for (int preset_id = 0; preset_id < PROGRESS_MARK_TOTAL_COUNT; preset_id++) { if (isProgressMarkSetFromAttribute(preset_id)) { - final Bitmap progressMarkBitmapById = getProgressMarkBitmapByPresetId(preset_id); - totalHeight += progressMarkBitmapById.getHeight(); - if (totalWidth < progressMarkBitmapById.getWidth()) { - totalWidth = progressMarkBitmapById.getWidth(); - } + final Bitmap b = getScaledProgressMarkBitmapByPresetId(preset_id); + totalHeight += b.getHeight(); + totalWidth = Math.max(totalWidth, b.getWidth()); } } } if (has_maps) { - final Bitmap hasMapsBitmap = getHasMapsBitmap(); - totalHeight += hasMapsBitmap.getHeight(); - if (totalWidth < hasMapsBitmap.getWidth()) { - totalWidth = hasMapsBitmap.getWidth(); - } + final Bitmap b = getScaledHasMapsBitmap(); + totalHeight += b.getHeight(); + totalWidth = Math.max(totalWidth, b.getWidth()); } setMeasuredDimension(totalWidth, totalHeight); @@ -185,70 +214,61 @@ private boolean isProgressMarkSetFromAttribute(final int preset_id) { @Override protected void onDraw(final Canvas canvas) { int totalHeight = 0; + + final int drawOffsetLeft = Math.round(0.5f * density * scale); + if (bookmark_count > 0) { - final Bitmap bookmarkBitmap = getBookmarkBitmap(); - canvas.drawBitmap(bookmarkBitmap, drawOffsetLeft, totalHeight, null); + final Bitmap b = getScaledBookmarkBitmap(); + canvas.drawBitmap(b, drawOffsetLeft, totalHeight, null); if (bookmark_count > 1) { - canvas.drawText(String.valueOf(bookmark_count), drawOffsetLeft + bookmarkBitmap.getWidth() / 2, totalHeight + bookmarkBitmap.getHeight() * 3/4, attributeCountPaintBookmark); + bookmarkCountPaint.setTextSize(COUNT_TEXT_SIZE_DP * density * scale); + canvas.drawText(String.valueOf(bookmark_count), drawOffsetLeft + b.getWidth() / 2, totalHeight + b.getHeight() * 3 / 4, bookmarkCountPaint); } - totalHeight += bookmarkBitmap.getHeight(); + totalHeight += b.getHeight(); } if (note_count > 0) { - final Bitmap noteBitmap = getNoteBitmap(); - canvas.drawBitmap(noteBitmap, drawOffsetLeft, totalHeight, null); + final Bitmap b = getScaledNoteBitmap(); + canvas.drawBitmap(b, drawOffsetLeft, totalHeight, null); if (note_count > 1) { - canvas.drawText(String.valueOf(note_count), drawOffsetLeft + noteBitmap.getWidth() / 2, totalHeight + noteBitmap.getHeight() * 7/10, attributeCountPaintNote); + noteCountPaint.setTextSize(COUNT_TEXT_SIZE_DP * density * scale); + canvas.drawText(String.valueOf(note_count), drawOffsetLeft + b.getWidth() / 2, totalHeight + b.getHeight() * 7 / 10, noteCountPaint); } - totalHeight += noteBitmap.getHeight(); + totalHeight += b.getHeight(); } if (progress_mark_bits != 0) { for (int preset_id = 0; preset_id < PROGRESS_MARK_TOTAL_COUNT; preset_id++) { if (isProgressMarkSetFromAttribute(preset_id)) { - final Bitmap progressMarkBitmapById = getProgressMarkBitmapByPresetId(preset_id); - final Long animationStartTime = progressMarkAnimationStartTimes.get(preset_id); - final Paint p; - if (animationStartTime == null) { - p = null; - } else { - final int animationElapsed = (int) (System.currentTimeMillis() - animationStartTime); - final int animationDuration = 800; - if (animationElapsed >= animationDuration) { - p = null; - } else { - alphaPaint.setAlpha(animationElapsed * 255 / animationDuration); - p = alphaPaint; - invalidate(); // animation is still running so request for invalidate - } - } - canvas.drawBitmap(progressMarkBitmapById, 0, totalHeight, p); - totalHeight += progressMarkBitmapById.getHeight(); + final Bitmap b = getScaledProgressMarkBitmapByPresetId(preset_id); + canvas.drawBitmap(b, 0, totalHeight, null); + totalHeight += b.getHeight(); } } } if (has_maps) { - final Bitmap hasMapsBitmap = getHasMapsBitmap(); - canvas.drawBitmap(hasMapsBitmap, drawOffsetLeft, totalHeight, null); + final Bitmap b = getScaledHasMapsBitmap(); + canvas.drawBitmap(b, drawOffsetLeft, totalHeight, null); //noinspection UnusedAssignment - totalHeight += hasMapsBitmap.getHeight(); + totalHeight += b.getHeight(); } } @Override public boolean onTouchEvent(final MotionEvent event) { - float y = event.getY(); - int totalHeight = 0; - if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP) { + final int action = MotionEventCompat.getActionMasked(event); + if (action == MotionEvent.ACTION_UP) { + int totalHeight = 0; + final float y = event.getY(); if (bookmark_count > 0) { - final Bitmap bookmarkBitmap = getBookmarkBitmap(); - totalHeight += bookmarkBitmap.getHeight(); + final Bitmap b = getScaledBookmarkBitmap(); + totalHeight += b.getHeight(); if (totalHeight > y) { attributeListener.onBookmarkAttributeClick(ari); return true; } } if (note_count > 0) { - final Bitmap noteBitmap = getNoteBitmap(); - totalHeight += noteBitmap.getHeight(); + final Bitmap b = getScaledNoteBitmap(); + totalHeight += b.getHeight(); if (totalHeight > y) { attributeListener.onNoteAttributeClick(ari); return true; @@ -257,8 +277,8 @@ public boolean onTouchEvent(final MotionEvent event) { if (progress_mark_bits != 0) { for (int preset_id = 0; preset_id < PROGRESS_MARK_TOTAL_COUNT; preset_id++) { if (isProgressMarkSetFromAttribute(preset_id)) { - final Bitmap progressMarkBitmapById = getProgressMarkBitmapByPresetId(preset_id); - totalHeight += progressMarkBitmapById.getHeight(); + final Bitmap b = getScaledProgressMarkBitmapByPresetId(preset_id); + totalHeight += b.getHeight(); if (totalHeight > y) { attributeListener.onProgressMarkAttributeClick(preset_id); return true; @@ -267,14 +287,14 @@ public boolean onTouchEvent(final MotionEvent event) { } } if (has_maps) { - final Bitmap hasMapsBitmap = getHasMapsBitmap(); - totalHeight += hasMapsBitmap.getHeight(); + final Bitmap b = getScaledHasMapsBitmap(); + totalHeight += b.getHeight(); if (totalHeight > y) { attributeListener.onHasMapsAttributeClick(ari); return true; } } - } else if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { + } else if (action == MotionEvent.ACTION_DOWN) { return true; } return false; @@ -285,10 +305,6 @@ public void setAttributeListener(VersesView.AttributeListener attributeListener, this.ari = ari; } - public static void startAnimationForProgressMark(final int preset_id) { - progressMarkAnimationStartTimes.put(preset_id, System.currentTimeMillis()); - } - public static int getDefaultProgressMarkStringResource(int preset_id) { switch (preset_id) { case 0: diff --git a/Alkitab/src/main/java/yuku/alkitab/base/widget/SingleViewVerseAdapter.java b/Alkitab/src/main/java/yuku/alkitab/base/widget/SingleViewVerseAdapter.java index eaf8571a5..9cbcab12f 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/widget/SingleViewVerseAdapter.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/widget/SingleViewVerseAdapter.java @@ -98,6 +98,7 @@ public int getItemViewType(final int position) { } final AttributeView attributeView = res.attributeView; + attributeView.setScale(scaleForAttributeView(S.applied.fontSize2dp)); attributeView.setBookmarkCount(bookmarkCountMap_ == null ? 0 : bookmarkCountMap_[id]); attributeView.setNoteCount(noteCountMap_ == null ? 0 : noteCountMap_[id]); attributeView.setProgressMarkBits(progressMarkBitsMap_ == null ? 0 : progressMarkBitsMap_[id]); @@ -234,6 +235,18 @@ public int getItemViewType(final int position) { } } + static float scaleForAttributeView(final float fontSizeDp) { + if (fontSizeDp >= 13 /* 76% */ && fontSizeDp < 22 /* 129% */) { + return 1.f; + } + + if (fontSizeDp < 10) return 0.5f; + if (fontSizeDp < 17) return 0.75f; + if (fontSizeDp >= 38) return 3.f; + if (fontSizeDp >= 30) return 2.f; + return 1.5f; // 22 to 30 + } + private void appendParallel(SpannableStringBuilder sb, String parallel) { int sb_len = sb.length(); diff --git a/Alkitab/src/main/res/layout/fragment_goto_grid.xml b/Alkitab/src/main/res/layout/fragment_goto_grid.xml index 6bc727f43..2ee23ca57 100644 --- a/Alkitab/src/main/res/layout/fragment_goto_grid.xml +++ b/Alkitab/src/main/res/layout/fragment_goto_grid.xml @@ -21,18 +21,22 @@ diff --git a/Alkitab/src/main/res/values/colors.xml b/Alkitab/src/main/res/values/colors.xml index e001528ce..8818d1422 100644 --- a/Alkitab/src/main/res/values/colors.xml +++ b/Alkitab/src/main/res/values/colors.xml @@ -1,5 +1,6 @@ #ffffff + #4db6ac \ No newline at end of file diff --git a/Alkitab/src/main/res/values/strings.xml b/Alkitab/src/main/res/values/strings.xml index 4b35ce54a..3403718e2 100644 --- a/Alkitab/src/main/res/values/strings.xml +++ b/Alkitab/src/main/res/values/strings.xml @@ -331,7 +331,8 @@ Finished: %s%% Target: %s%% On schedule - Behind the schedule: %s%% + Behind schedule: %s%% + Ahead of schedule: %s%% No more reading plans available. "Delete '%s'?" Change the starting date so that the first unread reading will be today? @@ -367,7 +368,7 @@ You already have this reading plan. Mark as read up to here? - "The version downloaded might need to be displayed with certain fonts. Go to the 'Text appearance' menu and select a font. Greek needs Ubuntu font. Burmese needs Parabaik, ParabaikSans, or Myanmar3. Tamil needs Akshar. Telugu needs Suranna." + "The version downloaded might need to be displayed with certain fonts. Go to the 'Display' menu and select a font. Greek needs Ubuntu font. Burmese needs Parabaik, ParabaikSans, or Myanmar3. Tamil needs Akshar. Telugu needs Suranna." Downloaded file seems to be corrupt. I/O error when saving downloaded version @@ -380,6 +381,7 @@ Search tips:\n\n+word for that exact word\n[q]city of david[q] for that exact phrase No verses found for ^1. Tap here to go to ^1. + Clear search history Transfer to another device Move all bookmarks, notes, and highlights to another phone or tablet. We recommend using Gmail/Email. diff --git a/AlkitabModel/src/main/java/yuku/alkitab/model/Version.java b/AlkitabModel/src/main/java/yuku/alkitab/model/Version.java index 08e34abae..4f9cc459e 100644 --- a/AlkitabModel/src/main/java/yuku/alkitab/model/Version.java +++ b/AlkitabModel/src/main/java/yuku/alkitab/model/Version.java @@ -63,8 +63,10 @@ public abstract class Version { */ public abstract int loadPericope(int bookId, int chapter_1, int[] aris, PericopeBlock[] pericopeBlocks, int max); + @Nullable public abstract SingleChapterVerses loadChapterText(Book book, int chapter_1); + @Nullable public abstract SingleChapterVerses loadChapterTextLowercased(Book book, int chapter_1); /** diff --git a/ImportedDesktopVerseUtil/src/main/java/yuku/alkitabconverter/util/DesktopVerseFinder.java b/ImportedDesktopVerseUtil/src/main/java/yuku/alkitabconverter/util/DesktopVerseFinder.java index ee9860b3b..ec722b34d 100644 --- a/ImportedDesktopVerseUtil/src/main/java/yuku/alkitabconverter/util/DesktopVerseFinder.java +++ b/ImportedDesktopVerseUtil/src/main/java/yuku/alkitabconverter/util/DesktopVerseFinder.java @@ -23,21 +23,26 @@ public interface DetectorListener { } // this array contains books that start with number, ex: 1 Kor - static String nofollow = "ch|chr|chron|chronicles|co|cor|corinthians|jhn|jn|jo|joh|john|kgs|ki|kin|kings|kor|korintus|pe|pet|peter|petrus|ptr|raj|raja|raja-raja|sa|sam|samuel|taw|tawarikh|tes|tesalonika|th|the|thes|thess|thessalonians|ti|tim|timothy|timotius|yoh|yohanes"; - + // "the" is removed from here, because texts like "Rom 10:16 the xyz" is linked as "Rom 10:16 the xyz" + static final String nofollow = "ch|chr|chron|chronicles|co|cor|corinthians|jhn|jn|jo|joh|john|kgs|ki|kin|kings|kor|korintus|pe|pet|peter|petrus|ptr|raj|raja|raja-raja|sa|sam|samuel|taw|tawarikh|tes|tesalonika|th|thes|thess|thessalonians|ti|tim|timothy|timotius|yoh|yohanes"; + + static final String WHITESPACE_NON_NEWLINE_CHAR = "[ \\t\\x0B\\f]"; + // this array contains all of book names, english and indonesian - static String bookNames = "genesis|gen|ge|gn|exodus|exod|exo|ex|leviticus|lev|lv|le|numbers|num|nmb|nu|deuteronomy|deut|deu|dt|de|joshua|josh|jos|judges|judg|jdg|ruth|rut|rth|ru|1 samuel|1samuel|1 sam|1sam|1 sa|1sa|i samuel|i sam|i sa|2 samuel|2samuel|2 sam|2sam|2 sa|2sa|ii samuel|ii sam|ii sa|1 kings|1kings|1 kin|1kin|1 kgs|1kgs|1 ki|1ki|i kings|i kin|i kgs|i ki|2 kings|2kings|2 kin|2kin|2 kgs|2kgs|2 ki|2ki|ii kings|ii kin|ii kgs|ii ki|1 chronicles|1chronicles|1 chron|1chron|1 chr|1chr|1 ch|1ch|i chronicles|i chron|i chr|i ch|2 chronicles|2chronicles|2 chron|2chron|2 chr|2chr|2 ch|2ch|ii chronicles|ii chron|ii chr|ii ch|ezra|ezr|nehemiah|neh|nh|ne|nehemia|esther|esth|est|es|ester|job|jb|psalms|psalm|psa|pss|ps|proverbs|proverb|prov|pro|pr|ecclesiastes|eccl|ecc|ec|songs of solomon|songsofsolomon|song of solomon|songofsolomon|song of songs|songofsongs|songs|song|son|sos|so|isaiah|isa|is|jeremiah|jer|je|lamentations|lam|la|ezekiel|ezek|eze|daniel|dan|dn|da|hosea|hos|ho|joel|joe|yl|amos|amo|am|obadiah|oba|ob|jonah|jon|micah|mikha|mic|mi|nahum|nah|na|habakkuk|habakuk|hab|zephaniah|zeph|zep|haggai|hagai|hag|zechariah|zech|zec|za|malachi|mal|matthew|mathew|matt|mat|mt|markus|mark|mar|mrk|mr|mk|luke|luk|lu|lk|john|joh|jhn|jn|acts of the apostles|actsoftheapostles|acts|act|ac|romans|rom|rm|ro|1 corinthians|1corinthians|1 cor|1cor|1 co|1co|i corinthians|i cor|i co|2 corinthians|2corinthians|2 cor|2cor|2 co|2co|ii corinthians|ii cor|ii co|galatians|galatia|gal|ga|ephesians|eph|ep|phillippians|philippians|phill|phil|phi|php|ph|colossians|col|co|1 thessalonians|1thessalonians|1 thess|1thess|1 thes|1thes|1 the|1the|1 th|1th|i thessalonians|i thess|i thes|i the|i th|2 thessalonians|2thessalonians|2 thess|2thess|2 thes|2thes|2 the|2the|2 th|2th|ii thessalonians|ii thess|ii thes|ii the|ii th|1 timothy|1timothy|1 tim|1tim|1 ti|1ti|i timothy|i tim|i ti|2 timothy|2timothy|2 tim|2tim|2 ti|2ti|ii timothy|ii tim|ii ti|titus|tit|philemon|phile|phm|hebrews|heb|he|james|jam|jas|jms|ja|jm|1 peter|1peter|1 pet|1pet|1 pe|1pe|i peter|i pet|i pe|1 ptr|1ptr|2 peter|2peter|2 pet|2pet|2 pe|2pe|ii peter|ii pet|ii pe|2 ptr|2ptr|1 john|1john|1 joh|1joh|1 jhn|1jhn|1 jo|1jo|1 jn|1jn|i john|i joh|i jhn|i jo|i jn|2 john|2john|2 joh|2joh|2 jhn|2jhn|2 jo|2jo|2 jn|2jn|ii john|ii joh|ii jhn|ii jo|ii jn|3 john|3john|3 joh|3joh|3 jhn|3jhn|3 jo|3jo|3 jn|3jn|iii john|iii joh|iii jhn|iii jo|iii jn|jude|jud|ju|revelations|revelation|rev|re|rv".replace(" ", "\\s+") - + "|" - + "kejadian|kej|kel|keluaran|im|imamat|bil|bilangan|ul|ulangan|yos|yosua|hak|hakim-hakim|rut|ru|1 samuel|1samuel|1 sam|1sam|1 sa|1sa|i samuel|i sam|i sa|2 samuel|2samuel|2 sam|2sam|2 sa|2sa|ii samuel|ii sam|ii sa|1 raj|1 raja|1raj|1raja|1 raja-raja|1raja-raja|2 raj|2 raja|2raj|2raja|2 raja-raja|2raja-raja|i raj|i raja|iraj|iraja|i raja-raja|iraja-raja|ii raj|ii raja|iiraj|iiraja|ii raja-raja|iiraja-raja|1 tawarikh|1tawarikh|1 taw|1taw|i tawarikh|i taw|2 tawarikh|2tawarikh|2 taw|2taw|ii tawarikh|ii taw|ezra|ezr|neh|nh|ne|nehemia|est|es|ester|ayub|ayb|ay|mazmur|maz|mzm|amsal|ams|pengkhotbah|pkh|kidung agung|kidungagung|kid|yesaya|yes|yeremia|yer|ratapan|rat|yehezkiel|yeh|hosea|hos|ho|yoel|yl|amos|amo|am|obaja|oba|ob|yunus|yun|mikha|mik|mi|nahum|nah|na|habakkuk|habakuk|hab|zefanya|zef|haggai|hagai|hag|zakharia|za|zak|maleakhi|mal|matius|mat|mt|markus|mark|mar|mrk|mr|mk|lukas|luk|lu|lk|yohanes|yoh|kisah para rasul|kisah rasul|kis|roma|rom|rm|ro|1 korintus|1korintus|1 kor|1kor|2 korintus|2korintus|2 kor|2kor|i korintus|ikorintus|i kor|ikor|ii korintus|iikorintus|ii kor|iikor|galatia|gal|ga|efesus|ef|filipi|flp|fil|kolose|kol|1 tesalonika|1tesalonika|1 tes|1tes|i tesalonika|i tes|2 tesalonika|2tesalonika|2 tes|2tes|ii tesalonika|ii tes|1timotius|1 timotius|1 tim|1tim|1 ti|1ti|i tim|i ti|i timotius|i tim|i ti|2timotius|2 timotius|2 tim|2tim|2 ti|2ti|ii timotius|ii tim|ii ti|titus|tit|filemon|flm|ibrani|ibr|yakobus|yak|1 pet|1pet|1 pe|1pe|1 petrus|1petrus|1 ptr|1ptr|2 pet|2pet|2 pe|2pe|ii peter|ii pet|ii pe|2 petrus|2petrus|2 ptr|2ptr|1 yohanes|1yohanes|1yoh|1 yoh|i yohanes|i yoh|2 yohanes|2yohanes|ii yohanes|ii yoh|2yoh|2 yoh|3 yohanes|3yohanes|3yoh|3 yoh|iii yohanes|iii yoh|yudas|yud|wahyu|why|wah".replace(" ", "[ \\t\\x0B\\f]+"); // no \r or \n allowed in between e.g. "1" and "John" as a book name + static final String bookNames = ( + "genesis|gen|ge|gn|exodus|exod|exo|ex|leviticus|lev|lv|le|numbers|num|nmb|nu|deuteronomy|deut|deu|dt|de|joshua|josh|jos|judges|judg|jdg|ruth|rut|rth|ru|1 samuel|1samuel|1 sam|1sam|1 sa|1sa|i samuel|i sam|i sa|2 samuel|2samuel|2 sam|2sam|2 sa|2sa|ii samuel|ii sam|ii sa|1 kings|1kings|1 kin|1kin|1 kgs|1kgs|1 ki|1ki|i kings|i kin|i kgs|i ki|2 kings|2kings|2 kin|2kin|2 kgs|2kgs|2 ki|2ki|ii kings|ii kin|ii kgs|ii ki|1 chronicles|1chronicles|1 chron|1chron|1 chr|1chr|1 ch|1ch|i chronicles|i chron|i chr|i ch|2 chronicles|2chronicles|2 chron|2chron|2 chr|2chr|2 ch|2ch|ii chronicles|ii chron|ii chr|ii ch|ezra|ezr|nehemiah|neh|nh|ne|nehemia|esther|esth|est|es|ester|job|jb|psalms|psalm|psa|pss|ps|proverbs|proverb|prov|pro|pr|ecclesiastes|eccl|ecc|ec|songs of solomon|songsofsolomon|song of solomon|songofsolomon|song of songs|songofsongs|songs|song|son|sos|so|isaiah|isa|is|jeremiah|jer|je|lamentations|lam|la|ezekiel|ezek|eze|daniel|dan|dn|da|hosea|hos|ho|joel|joe|yl|amos|amo|am|obadiah|oba|ob|jonah|jon|micah|mikha|mic|mi|nahum|nah|na|habakkuk|habakuk|hab|zephaniah|zeph|zep|haggai|hagai|hag|zechariah|zech|zec|za|malachi|mal|matthew|mathew|matt|mat|mt|markus|mark|mar|mrk|mr|mk|luke|luk|lu|lk|john|joh|jhn|jn|acts of the apostles|actsoftheapostles|acts|act|ac|romans|rom|rm|ro|1 corinthians|1corinthians|1 cor|1cor|1 co|1co|i corinthians|i cor|i co|2 corinthians|2corinthians|2 cor|2cor|2 co|2co|ii corinthians|ii cor|ii co|galatians|galatia|gal|ga|ephesians|eph|ep|phillippians|philippians|phill|phil|phi|php|ph|colossians|col|co|1 thessalonians|1thessalonians|1 thess|1thess|1 thes|1thes|1 the|1the|1 th|1th|i thessalonians|i thess|i thes|i the|i th|2 thessalonians|2thessalonians|2 thess|2thess|2 thes|2thes|2 the|2the|2 th|2th|ii thessalonians|ii thess|ii thes|ii the|ii th|1 timothy|1timothy|1 tim|1tim|1 ti|1ti|i timothy|i tim|i ti|2 timothy|2timothy|2 tim|2tim|2 ti|2ti|ii timothy|ii tim|ii ti|titus|tit|philemon|phile|phm|hebrews|heb|he|james|jam|jas|jms|ja|jm|1 peter|1peter|1 pet|1pet|1 pe|1pe|i peter|i pet|i pe|1 ptr|1ptr|2 peter|2peter|2 pet|2pet|2 pe|2pe|ii peter|ii pet|ii pe|2 ptr|2ptr|1 john|1john|1 joh|1joh|1 jhn|1jhn|1 jo|1jo|1 jn|1jn|i john|i joh|i jhn|i jo|i jn|2 john|2john|2 joh|2joh|2 jhn|2jhn|2 jo|2jo|2 jn|2jn|ii john|ii joh|ii jhn|ii jo|ii jn|3 john|3john|3 joh|3joh|3 jhn|3jhn|3 jo|3jo|3 jn|3jn|iii john|iii joh|iii jhn|iii jo|iii jn|jude|jud|ju|revelations|revelation|rev|re|rv" + + "|" + + "kejadian|kej|kel|keluaran|im|imamat|bil|bilangan|ul|ulangan|yos|yosua|hak|hakim-hakim|rut|ru|1 samuel|1samuel|1 sam|1sam|1 sa|1sa|i samuel|i sam|i sa|2 samuel|2samuel|2 sam|2sam|2 sa|2sa|ii samuel|ii sam|ii sa|1 raj|1 raja|1raj|1raja|1 raja-raja|1raja-raja|2 raj|2 raja|2raj|2raja|2 raja-raja|2raja-raja|i raj|i raja|iraj|iraja|i raja-raja|iraja-raja|ii raj|ii raja|iiraj|iiraja|ii raja-raja|iiraja-raja|1 tawarikh|1tawarikh|1 taw|1taw|i tawarikh|i taw|2 tawarikh|2tawarikh|2 taw|2taw|ii tawarikh|ii taw|ezra|ezr|neh|nh|ne|nehemia|est|es|ester|ayub|ayb|ay|mazmur|maz|mzm|amsal|ams|pengkhotbah|pkh|kidung agung|kidungagung|kid|yesaya|yes|yeremia|yer|ratapan|rat|yehezkiel|yeh|hosea|hos|ho|yoel|yl|amos|amo|am|obaja|oba|ob|yunus|yun|mikha|mik|mi|nahum|nah|na|habakkuk|habakuk|hab|zefanya|zef|haggai|hagai|hag|zakharia|za|zak|maleakhi|mal|matius|mat|mt|markus|mark|mar|mrk|mr|mk|lukas|luk|lu|lk|yohanes|yoh|kisah para rasul|kisah rasul|kis|roma|rom|rm|ro|1 korintus|1korintus|1 kor|1kor|2 korintus|2korintus|2 kor|2kor|i korintus|ikorintus|i kor|ikor|ii korintus|iikorintus|ii kor|iikor|galatia|gal|ga|efesus|ef|filipi|flp|fil|kolose|kol|1 tesalonika|1tesalonika|1 tes|1tes|i tesalonika|i tes|2 tesalonika|2tesalonika|2 tes|2tes|ii tesalonika|ii tes|1timotius|1 timotius|1 tim|1tim|1 ti|1ti|i tim|i ti|i timotius|i tim|i ti|2timotius|2 timotius|2 tim|2tim|2 ti|2ti|ii timotius|ii tim|ii ti|titus|tit|filemon|flm|ibrani|ibr|yakobus|yak|1 pet|1pet|1 pe|1pe|1 petrus|1petrus|1 ptr|1ptr|2 pet|2pet|2 pe|2pe|ii peter|ii pet|ii pe|2 petrus|2petrus|2 ptr|2ptr|1 yohanes|1yohanes|1yoh|1 yoh|i yohanes|i yoh|2 yohanes|2yohanes|ii yohanes|ii yoh|2yoh|2 yoh|3 yohanes|3yohanes|3yoh|3 yoh|iii yohanes|iii yoh|yudas|yud|wahyu|why|wah" + ).replace(" ", WHITESPACE_NON_NEWLINE_CHAR + "+"); // no \r or \n allowed in between e.g. "1" and "John" as a book name ///////////////////////////////////// 1 something before (or nothing) ///////////////////////////////////// 2 complete verse address (book chapter verse) ///////////////////////////////////// 3 book name with optional period and spaces after it - ///////////////////////////////////// 4 book name 5 numbers (chapter or chapter:verse, with ',' or ';' or 'dan') which is not followed by nofollow - static Pattern reg = Pattern.compile("(\\b)(((" + bookNames + ")\\.?\\s+)(\\d+(?:(?:-|:|(?:;\\s*\\d+:\\s*)|,|\\.|\\d|dan|\\s)+\\d+)?)(?!\\s*(?:" + nofollow + ")\\.?\\s))", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + ///////////////////////////////////// 4 book name 5 numbers (chapter or chapter:verse, with ',' or ';' or 'dan') which is not followed by nofollow + static final Pattern reg = Pattern.compile("(\\b)(((" + bookNames + ")\\.?" + WHITESPACE_NON_NEWLINE_CHAR + "+)(\\d+(?:(?:-|:|(?:;" + WHITESPACE_NON_NEWLINE_CHAR + "*\\d+:" + WHITESPACE_NON_NEWLINE_CHAR + "*)|,|\\.|\\d|dan|" + WHITESPACE_NON_NEWLINE_CHAR + ")+\\d+)?)(?!" + WHITESPACE_NON_NEWLINE_CHAR + "*(?:" + nofollow + ")\\.?\\s))", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); public static void findInText(CharSequence input, DetectorListener detectorListener) { - Matcher match_1 = reg.matcher(input); + final Matcher match_1 = reg.matcher(input); while (match_1.find()) { // to solve the problem of "Dan" book diff --git a/README.md b/README.md index 25a270122..ec036fa99 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ Get in on Google Play: Changelog and Development Blog - Discussion Group and Beta Testers -By the way, Alkitab is the Indonesian word for Bible. +By the way, Alkitab is the Indonesian word for the Bible. Bible translations/versions --------------------------- -This app natively uses *.yes* files for the Bible text. You can create a *.yes* file easily by preparing a plain text file. See instructions. +This app natively uses *.yes* files for the Bible text. You can create a *.yes* file easily by preparing a plain text file. See this page for instructions. You can also convert PalmBible+ PDB files using the built-in converter in the app or use the pdb2yes online converter that produces compressed YES files. diff --git a/ybuild/overlay/org.sabda.alkitab-market/version_config.json b/ybuild/overlay/org.sabda.alkitab-market/version_config.json index 2298e345f..e4244af05 100644 --- a/ybuild/overlay/org.sabda.alkitab-market/version_config.json +++ b/ybuild/overlay/org.sabda.alkitab-market/version_config.json @@ -5,9 +5,9 @@ {"locale": "ban", "preset_name": "ban-bali", "shortName": "BALI", "longName": "Bali", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bali"}, {"locale": "ban", "preset_name": "ban-bali90", "shortName": "BALI90", "longName": "Bali 1990", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bali (Bahasa Sehari-hari) - Cakepan Suci"}, {"locale": "ptu", "preset_name": "ptu-bambam", "shortName": "BAMBAM", "longName": "Bambam", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bambam"}, -{"locale": "btd", "preset_name": "btd-dairi", "shortName": "DAIRI", "longName": "Batak-Dairi", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Dairi"}, -{"locale": "btx", "preset_name": "btx-karo", "shortName": "KARO", "longName": "Batak-Karo", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Karo"}, -{"locale": "bbc", "preset_name": "bbc-toba", "shortName": "TOBA", "longName": "Batak-Toba", "modifyTime": 1442534400, "description": "Alkitab Batak Toba"}, +{"locale": "btd", "preset_name": "btd-dairi", "shortName": "DAIRI", "longName": "Batak-Dairi", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Dairi"}, +{"locale": "btx", "preset_name": "btx-karo", "shortName": "KARO", "longName": "Pustaka Si Badia", "modifyTime": 1401926400, "description": "Alkitab Bahasa Karo"}, +{"locale": "bbc", "preset_name": "bbc-toba", "shortName": "TOBA", "longName": "Batak-Toba", "modifyTime": 1442534400, "description": "Alkitab Batak Toba"}, {"locale": "bts", "preset_name": "bts-simalungun", "shortName": "SIMA", "longName": "Batak-Simalungun", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Simalungun"}, {"locale": "bug", "preset_name": "bug-bugis", "shortName": "BUGIS", "longName": "Bugis", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bugis"}, {"locale": "nij", "preset_name": "nij-ngaju", "shortName": "NGAJU", "longName": "Dayak-Ngaju", "modifyTime": 1401926400, "description": "Alkitab Bahasa Dayak Ngaju"}, @@ -475,7 +475,7 @@ "ban": "Bali", "ptu": "Bambam", "btd": "Batak-Dairi", - "btx": "Batak-Karo", + "btx": "Karo", "bbc": "Batak-Toba", "bts": "Batak-Simalungun", "bug": "Bugis", diff --git a/ybuild/overlay/org.sabda.online-market/version_config.json b/ybuild/overlay/org.sabda.online-market/version_config.json index 3d6791acb..9f0840763 100644 --- a/ybuild/overlay/org.sabda.online-market/version_config.json +++ b/ybuild/overlay/org.sabda.online-market/version_config.json @@ -475,7 +475,7 @@ "ban": "Bali", "ptu": "Bambam", "btd": "Batak-Dairi", - "btx": "Batak-Karo", + "btx": "Karo", "bbc": "Batak-Toba", "bts": "Batak-Simalungun", "bug": "Bugis", diff --git a/ybuild/overlay/yuku.alkitab-market/version_config.json b/ybuild/overlay/yuku.alkitab-market/version_config.json index 2298e345f..f4ae09861 100644 --- a/ybuild/overlay/yuku.alkitab-market/version_config.json +++ b/ybuild/overlay/yuku.alkitab-market/version_config.json @@ -4,10 +4,10 @@ {"locale": "blz", "preset_name": "blz-balantak", "shortName": "BLNTK", "longName": "Balantak", "modifyTime": 1401926400, "description": "Alkitab Bahasa Balantak"}, {"locale": "ban", "preset_name": "ban-bali", "shortName": "BALI", "longName": "Bali", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bali"}, {"locale": "ban", "preset_name": "ban-bali90", "shortName": "BALI90", "longName": "Bali 1990", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bali (Bahasa Sehari-hari) - Cakepan Suci"}, -{"locale": "ptu", "preset_name": "ptu-bambam", "shortName": "BAMBAM", "longName": "Bambam", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bambam"}, -{"locale": "btd", "preset_name": "btd-dairi", "shortName": "DAIRI", "longName": "Batak-Dairi", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Dairi"}, -{"locale": "btx", "preset_name": "btx-karo", "shortName": "KARO", "longName": "Batak-Karo", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Karo"}, -{"locale": "bbc", "preset_name": "bbc-toba", "shortName": "TOBA", "longName": "Batak-Toba", "modifyTime": 1442534400, "description": "Alkitab Batak Toba"}, +{"locale": "ptu", "preset_name": "ptu-bambam", "shortName": "BAMBAM", "longName": "Bambam", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bambam"}, +{"locale": "btd", "preset_name": "btd-dairi", "shortName": "DAIRI", "longName": "Batak-Dairi", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Dairi"}, +{"locale": "btx", "preset_name": "btx-karo", "shortName": "KARO", "longName": "Pustaka Si Badia", "modifyTime": 1401926400, "description": "Alkitab Bahasa Karo"}, +{"locale": "bbc", "preset_name": "bbc-toba", "shortName": "TOBA", "longName": "Batak-Toba", "modifyTime": 1442534400, "description": "Alkitab Batak Toba"}, {"locale": "bts", "preset_name": "bts-simalungun", "shortName": "SIMA", "longName": "Batak-Simalungun", "modifyTime": 1401926400, "description": "Alkitab Bahasa Batak Simalungun"}, {"locale": "bug", "preset_name": "bug-bugis", "shortName": "BUGIS", "longName": "Bugis", "modifyTime": 1401926400, "description": "Alkitab Bahasa Bugis"}, {"locale": "nij", "preset_name": "nij-ngaju", "shortName": "NGAJU", "longName": "Dayak-Ngaju", "modifyTime": 1401926400, "description": "Alkitab Bahasa Dayak Ngaju"}, @@ -475,7 +475,7 @@ "ban": "Bali", "ptu": "Bambam", "btd": "Batak-Dairi", - "btx": "Batak-Karo", + "btx": "Karo", "bbc": "Batak-Toba", "bts": "Batak-Simalungun", "bug": "Bugis", diff --git a/ybuild/overlay/yuku.alkitab.kjv-market/version_config.json b/ybuild/overlay/yuku.alkitab.kjv-market/version_config.json index 3d6791acb..9f0840763 100644 --- a/ybuild/overlay/yuku.alkitab.kjv-market/version_config.json +++ b/ybuild/overlay/yuku.alkitab.kjv-market/version_config.json @@ -475,7 +475,7 @@ "ban": "Bali", "ptu": "Bambam", "btd": "Batak-Dairi", - "btx": "Batak-Karo", + "btx": "Karo", "bbc": "Batak-Toba", "bts": "Batak-Simalungun", "bug": "Bugis",