From 79e153e6e54d268302e36bce04f035f2f936370f Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Wed, 23 Dec 2015 17:07:30 +0800 Subject: [PATCH 01/25] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From 87b3dd4ae1e27ee712b03d132156b9300ff88cf1 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Wed, 23 Dec 2015 17:08:19 +0800 Subject: [PATCH 02/25] Fix a bug in marker importer where label assignment is not stored in some (almost random) cases due to wrong count query. --- .../alkitab/base/util/BookmarkImporter.java | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) 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..b12fa5b28 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java @@ -238,34 +238,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 +270,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 +290,7 @@ public static void importBookmarks(List markers, TObjectIntHashMap Date: Wed, 23 Dec 2015 17:14:32 +0800 Subject: [PATCH 03/25] Reload labels list after importing markers --- .../src/main/java/yuku/alkitab/base/ac/MarkersActivity.java | 3 +-- .../main/java/yuku/alkitab/base/util/BookmarkImporter.java | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) 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/util/BookmarkImporter.java b/Alkitab/src/main/java/yuku/alkitab/base/util/BookmarkImporter.java index b12fa5b28..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(); } From 58926baf3a74b7527c11690921ebdb445f5228aa Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Wed, 23 Dec 2015 18:10:35 +0800 Subject: [PATCH 04/25] =?UTF-8?q?Put=20=E2=95=B3=20on=20grid=20navigation?= =?UTF-8?q?=20selected=20book/chapter=20to=20indicate=20that=20if=20you=20?= =?UTF-8?q?click=20on=20it,=20the=20selection=20will=20be=20cancelled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/yuku/alkitab/base/fr/GotoGridFragment.java | 12 ++---------- Alkitab/src/main/res/layout/fragment_goto_grid.xml | 8 ++++++-- 2 files changed, 8 insertions(+), 12 deletions(-) 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/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 @@ From 01c9591d9e52031b0cffd82b23bee70be518f26e Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 12:02:41 +0800 Subject: [PATCH 05/25] Support "ahead of schedule" on reading plans. --- .../alkitab/base/ac/ReadingPlanActivity.java | 53 +++++++++++-------- Alkitab/src/main/res/values/colors.xml | 1 + Alkitab/src/main/res/values/strings.xml | 3 +- 3 files changed, 33 insertions(+), 24 deletions(-) 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..075cc64d7 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(originalCommentTextColor); + } else if (actualPercentage < targetPercentage) { + tComment.setText(getString(R.string.rp_commentBehindSchedule, String.format(Locale.US, "%.2f", targetPercentage - actualPercentage))); + tComment.setTextColor(getResources().getColor(R.color.escape)); } 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/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..e3abdf73a 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? From 26b45f3e25cf6fedfc9c46c4879414b7173af88f Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 16:43:48 +0800 Subject: [PATCH 06/25] Color "ahead of schedule" and "on schedule" differently than behind schedule. --- .../main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 075cc64d7..ee905156f 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/ReadingPlanActivity.java @@ -775,10 +775,10 @@ public void bindView(final View res, final int position, final ViewGroup parent) if (actualPercentage == targetPercentage) { tComment.setText(R.string.rp_commentOnSchedule); - tComment.setTextColor(originalCommentTextColor); + 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(getResources().getColor(R.color.escape)); + tComment.setTextColor(originalCommentTextColor); } else { tComment.setText(getString(R.string.rp_commentAheadSchedule, String.format(Locale.US, "%.2f", actualPercentage - targetPercentage))); tComment.setTextColor(getResources().getColor(R.color.escape)); From 60308383dd110bc3dbb6bdf95b18d6e330387e2c Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 16:44:08 +0800 Subject: [PATCH 07/25] String: "Text appearance" -> "Display" --- Alkitab/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Alkitab/src/main/res/values/strings.xml b/Alkitab/src/main/res/values/strings.xml index e3abdf73a..5d9152809 100644 --- a/Alkitab/src/main/res/values/strings.xml +++ b/Alkitab/src/main/res/values/strings.xml @@ -368,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 From 21b778025b477d43f6560868a1cdc44cbeed31e3 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 17:14:57 +0800 Subject: [PATCH 08/25] Support scaling of attribute view --- .../alkitab/base/widget/AttributeView.java | 162 ++++++++++-------- 1 file changed, 95 insertions(+), 67 deletions(-) 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..3008d3927 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java @@ -20,15 +20,20 @@ public class AttributeView extends View { 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 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 alphaPaint = new Paint(); - static Paint attributeCountPaintBookmark = new Paint(); + static Paint attributeCountPaintBookmark; static Paint attributeCountPaintNote; static { + attributeCountPaintBookmark = new Paint(); attributeCountPaintBookmark.setTypeface(Typeface.DEFAULT_BOLD); attributeCountPaintBookmark.setColor(0xff000000); attributeCountPaintBookmark.setTextSize(App.context.getResources().getDisplayMetrics().density * 12.f); @@ -42,6 +47,7 @@ public class AttributeView extends View { int note_count; int progress_mark_bits; boolean has_maps; + float scale = 2.f; private VersesView.AttributeListener attributeListener; private int ari; @@ -110,32 +116,61 @@ public boolean isShowingSomething() { return bookmark_count > 0 || note_count > 0 || ((progress_mark_bits & PROGRESS_MARK_BIT_MASK) != 0); } - Bitmap getBookmarkBitmap() { - if (bookmarkBitmap == null) { - bookmarkBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_bookmark); + static Bitmap scale(Bitmap original, float scale) { + if (scale == 1.f) { + return Bitmap.createBitmap(original); } - return bookmarkBitmap; + + 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); + } + + if (scaledBookmarkBitmap == null || scaledBookmarkBitmap.getWidth() != Math.round(originalBookmarkBitmap.getWidth() * scale)) { + scaledBookmarkBitmap = scale(originalBookmarkBitmap, scale); } - return noteBitmap; + + return scaledBookmarkBitmap; } - Bitmap getProgressMarkBitmapByPresetId(int preset_id) { - if (progressMarkBitmap[preset_id] == null) { - progressMarkBitmap[preset_id] = BitmapFactory.decodeResource(getResources(), getProgressMarkIconResource(preset_id)); + Bitmap getScaledNoteBitmap() { + if (originalNoteBitmap == null) { + originalNoteBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_note); + } + + if (scaledNoteBitmap == null || scaledBookmarkBitmap.getWidth() != Math.round(originalNoteBitmap.getWidth() * scale)) { + scaledNoteBitmap = scale(originalNoteBitmap, scale); } - return progressMarkBitmap[preset_id]; + + return scaledNoteBitmap; } - Bitmap getHasMapsBitmap() { - if (hasMapsBitmap == null) { - hasMapsBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_has_maps); + Bitmap getScaledProgressMarkBitmapByPresetId(int preset_id) { + if (originalProgressMarkBitmaps[preset_id] == null) { + originalProgressMarkBitmaps[preset_id] = BitmapFactory.decodeResource(getResources(), getProgressMarkIconResource(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 hasMapsBitmap; + + return scaledProgressMarkBitmaps[preset_id]; + } + + 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 scaledHasMapsBitmap; } @Override @@ -143,36 +178,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); @@ -186,25 +213,25 @@ private boolean isProgressMarkSetFromAttribute(final int preset_id) { protected void onDraw(final Canvas canvas) { int totalHeight = 0; 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); + canvas.drawText(String.valueOf(bookmark_count), drawOffsetLeft + b.getWidth() / 2, totalHeight + b.getHeight() * 3 / 4, attributeCountPaintBookmark); } - 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); + canvas.drawText(String.valueOf(note_count), drawOffsetLeft + b.getWidth() / 2, totalHeight + b.getHeight() * 7 / 10, attributeCountPaintNote); } - 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 Bitmap b = getScaledProgressMarkBitmapByPresetId(preset_id); final Long animationStartTime = progressMarkAnimationStartTimes.get(preset_id); final Paint p; if (animationStartTime == null) { @@ -220,35 +247,36 @@ protected void onDraw(final Canvas canvas) { invalidate(); // animation is still running so request for invalidate } } - canvas.drawBitmap(progressMarkBitmapById, 0, totalHeight, p); - totalHeight += progressMarkBitmapById.getHeight(); + canvas.drawBitmap(b, 0, totalHeight, p); + 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 +285,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 +295,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; From 366408dd71c0b82affc5e4299a7b9a0773e41e03 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 17:16:25 +0800 Subject: [PATCH 09/25] Remove pin alpha-animation code --- .../alkitab/base/widget/AttributeView.java | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) 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 3008d3927..a0e9211bf 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java @@ -8,14 +8,12 @@ import android.graphics.Typeface; import android.support.v4.view.MotionEventCompat; import android.util.AttributeSet; -import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; import yuku.alkitab.base.App; import yuku.alkitab.debug.R; public class AttributeView extends View { - 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); @@ -28,7 +26,6 @@ public class AttributeView extends View { static Bitmap[] scaledProgressMarkBitmaps = new Bitmap[PROGRESS_MARK_TOTAL_COUNT]; static Bitmap originalHasMapsBitmap = null; static Bitmap scaledHasMapsBitmap = null; - static Paint alphaPaint = new Paint(); static Paint attributeCountPaintBookmark; static Paint attributeCountPaintNote; @@ -52,7 +49,6 @@ public class AttributeView extends View { private VersesView.AttributeListener attributeListener; private int ari; - private static SparseArray progressMarkAnimationStartTimes = new SparseArray<>(); private int drawOffsetLeft; public AttributeView(final Context context) { @@ -232,22 +228,7 @@ protected void onDraw(final Canvas canvas) { for (int preset_id = 0; preset_id < PROGRESS_MARK_TOTAL_COUNT; preset_id++) { if (isProgressMarkSetFromAttribute(preset_id)) { final Bitmap b = getScaledProgressMarkBitmapByPresetId(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(b, 0, totalHeight, p); + canvas.drawBitmap(b, 0, totalHeight, null); totalHeight += b.getHeight(); } } @@ -313,10 +294,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: From e1e072d95a804ff2437ede53e79e1a08030d9fdf Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 17:17:23 +0800 Subject: [PATCH 10/25] Fix bug collapsed verse does not show hasmaps bitmap --- .../src/main/java/yuku/alkitab/base/widget/AttributeView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a0e9211bf..d04cb7088 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/widget/AttributeView.java @@ -109,7 +109,7 @@ 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; } static Bitmap scale(Bitmap original, float scale) { From 057f9537abdc5bc74145f473efe9c82a72c92f6f Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Tue, 29 Dec 2015 17:49:12 +0800 Subject: [PATCH 11/25] Attributes like bookmark/notes/pins/hasmaps is scaled too when text is scaled --- .../alkitab/base/widget/AttributeView.java | 61 +++++++++++-------- .../base/widget/SingleViewVerseAdapter.java | 13 ++++ 2 files changed, 49 insertions(+), 25 deletions(-) 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 d04cb7088..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,15 +8,20 @@ import android.graphics.Typeface; import android.support.v4.view.MotionEventCompat; import android.util.AttributeSet; +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); + private static final float COUNT_TEXT_SIZE_DP = 12.f; static Bitmap originalBookmarkBitmap = null; static Bitmap scaledBookmarkBitmap = null; @@ -26,46 +31,43 @@ public class AttributeView extends View { static Bitmap[] scaledProgressMarkBitmaps = new Bitmap[PROGRESS_MARK_TOTAL_COUNT]; static Bitmap originalHasMapsBitmap = null; static Bitmap scaledHasMapsBitmap = null; - static Paint attributeCountPaintBookmark; - static Paint attributeCountPaintNote; - - static { - attributeCountPaintBookmark = new Paint(); - 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); + 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 = 2.f; + float scale = 1.f; private VersesView.AttributeListener attributeListener; private int ari; - 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() { @@ -113,6 +115,10 @@ public boolean isShowingSomething() { } static Bitmap scale(Bitmap original, float scale) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "@@scale Scale needed. Called with scale=" + scale); + } + if (scale == 1.f) { return Bitmap.createBitmap(original); } @@ -138,7 +144,7 @@ Bitmap getScaledNoteBitmap() { originalNoteBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_attr_note); } - if (scaledNoteBitmap == null || scaledBookmarkBitmap.getWidth() != Math.round(originalNoteBitmap.getWidth() * scale)) { + if (scaledNoteBitmap == null || scaledNoteBitmap.getWidth() != Math.round(originalNoteBitmap.getWidth() * scale)) { scaledNoteBitmap = scale(originalNoteBitmap, scale); } @@ -208,11 +214,15 @@ 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 b = getScaledBookmarkBitmap(); canvas.drawBitmap(b, drawOffsetLeft, totalHeight, null); if (bookmark_count > 1) { - canvas.drawText(String.valueOf(bookmark_count), drawOffsetLeft + b.getWidth() / 2, totalHeight + b.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 += b.getHeight(); } @@ -220,7 +230,8 @@ protected void onDraw(final Canvas canvas) { final Bitmap b = getScaledNoteBitmap(); canvas.drawBitmap(b, drawOffsetLeft, totalHeight, null); if (note_count > 1) { - canvas.drawText(String.valueOf(note_count), drawOffsetLeft + b.getWidth() / 2, totalHeight + b.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 += b.getHeight(); } 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(); From cbff6119adca5993f2158d2527f17da4ec93ad9f Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Wed, 6 Jan 2016 15:26:32 +0800 Subject: [PATCH 12/25] Updating a Bible version no longer changes its ordering --- .../alkitab/base/ac/VersionsActivity.java | 3 ++- .../br/VersionDownloadCompleteReceiver.java | 3 ++- .../yuku/alkitab/base/model/MVersionDb.java | 4 +++ .../yuku/alkitab/base/storage/InternalDb.java | 27 ++++++++++++++----- 4 files changed, 28 insertions(+), 9 deletions(-) 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/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/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(); From 7c8c1b53b34e49e017f4d1d3e10c06eadfdc7db1 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 7 Jan 2016 13:08:34 +0800 Subject: [PATCH 13/25] Add entry to clear history at the end of search suggestion dropdown --- .../yuku/alkitab/base/ac/SearchActivity.java | 57 +++++++++++++++---- Alkitab/src/main/res/values/strings.xml | 1 + 2 files changed, 46 insertions(+), 12 deletions(-) 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..d43f2bee2 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,8 @@ import android.database.MatrixCursor; import android.os.AsyncTask; import android.os.Bundle; +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 +17,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; @@ -56,13 +59,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 +211,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 +243,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 +314,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; } @@ -750,7 +779,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 +788,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 diff --git a/Alkitab/src/main/res/values/strings.xml b/Alkitab/src/main/res/values/strings.xml index 5d9152809..3403718e2 100644 --- a/Alkitab/src/main/res/values/strings.xml +++ b/Alkitab/src/main/res/values/strings.xml @@ -381,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. From f926542a7ec7c016d1c5510c93b2efc4d2cec318 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 7 Jan 2016 13:49:06 +0800 Subject: [PATCH 14/25] Show debug info after searching --- .../yuku/alkitab/base/ac/SearchActivity.java | 32 ++++++++- .../yuku/alkitab/base/util/SearchEngine.java | 71 ++++++++----------- 2 files changed, 58 insertions(+), 45 deletions(-) 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 d43f2bee2..dff9c817a 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,7 @@ 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; @@ -48,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; @@ -683,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) { @@ -745,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(); 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..8bdb72d48 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -75,10 +75,10 @@ public RevIndex() { public static IntArrayList searchByGrep(final Version version, final Query query) { String[] words = QueryTokenizer.tokenize(query.query_string); - // urutkan berdasarkan panjang, lalu abjad + // sort by word length, then alphabetically Arrays.sort(words, (object1, object2) -> { - int len1 = object1.length(); - int len2 = object2.length(); + final int len1 = object1.length(); + final int len2 = object2.length(); if (len1 > len2) return -1; if (len1 == len2) { @@ -103,35 +103,23 @@ public static IntArrayList searchByGrep(final Version version, final Query query // 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 word : words) { + final 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"); } } - + return result; } @@ -196,24 +184,21 @@ private static int nextAri(IntArrayList source, int[] ppos, int lastAriBc) { static IntArrayList searchByGrepInside(final Version version, String word, final IntArrayList source, final SparseBooleanArray bookIds) { final IntArrayList res = new IntArrayList(); - boolean hasPlus = false; - - if (QueryTokenizer.isPlussedToken(word)) { - hasPlus = true; + final boolean hasPlus = QueryTokenizer.isPlussedToken(word); + + if (hasPlus) { word = QueryTokenizer.tokenWithoutPlus(word); } - + 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); + final 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); @@ -231,12 +216,12 @@ static IntArrayList searchByGrepInside(final Version version, String word, final 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. final Book book = version.getBook(Ari.toBook(curAriBc)); - int chapter_1 = Ari.toChapter(curAriBc); + final int chapter_1 = Ari.toChapter(curAriBc); final String oneChapter = version.loadChapterTextLowercasedWithoutSplit(book, chapter_1); if (oneChapter.contains(word)) { @@ -491,7 +476,7 @@ 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"); From d422247ce768b45eaf61fde91ccecf8b13ac55ea Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 7 Jan 2016 14:28:19 +0800 Subject: [PATCH 15/25] Clean up search engine. Remove useless optimization --- .../yuku/alkitab/base/util/SearchEngine.java | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) 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 8bdb72d48..f0e0ee192 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -198,11 +198,8 @@ static IntArrayList searchByGrepInside(final Version version, String word, final for (int chapter_1 = 1; chapter_1 <= book.chapter_count; chapter_1++) { // try to find it wholly in a chapter - final 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, word, hasPlus, ariBc, res); } if (BuildConfig.DEBUG) Log.d(TAG, "searchByGrepInside book " + book.shortName + " done. res.size = " + res.size()); @@ -219,15 +216,11 @@ static IntArrayList searchByGrepInside(final Version version, String word, final 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)); final 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); - } + + searchByGrepForOneChapter(version, book, chapter_1, word, hasPlus, curAriBc, res); count++; } @@ -238,38 +231,53 @@ static IntArrayList searchByGrepInside(final Version version, String word, final return res; } - private static void searchByGrepInChapter(String oneChapter, String word, IntArrayList res, int base, boolean hasPlus) { + /** + * @param word searched word without plusses + * @param res (output) result aris + * @param ariBc book-chapter ari, with verse must be set to 0 + * @param hasPlus whether the word had plus + */ + private static void searchByGrepForOneChapter(final Version version, final Book book, final int chapter_1, final String word, 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; - + + // Initial search int posWord; if (hasPlus) { posWord = indexOfWholeWord(oneChapter, word, 0); - if (posWord == -1) { - return; - } } else { - posWord = oneChapter.indexOf(word); + posWord = oneChapter.indexOf(word, 0); } - + + if (posWord == -1) { + // initial search does not return results. It means the whole chapter does not contain the word. + return; + } + int posN = oneChapter.indexOf(0x0a); - + while (true) { if (posN < posWord) { 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); } else { - posWord = oneChapter.indexOf(word, posWord+1); + posWord = oneChapter.indexOf(word, posWord + 1); } if (posWord == -1) { return; @@ -277,7 +285,7 @@ private static void searchByGrepInChapter(String oneChapter, String word, IntArr } } } - + public static IntArrayList searchByRevIndex(final Version version, final Query query) { TimingLogger timing = new TimingLogger("RevIndex", "searchByRevIndex"); RevIndex revIndex; @@ -591,10 +599,8 @@ private static RevIndex loadRevIndex() { */ public static boolean satisfiesQuery(String s, String[] words) { for (String word: words) { - boolean hasPlus = false; - - if (QueryTokenizer.isPlussedToken(word)) { - hasPlus = true; + final boolean hasPlus = QueryTokenizer.isPlussedToken(word); + if (hasPlus) { word = QueryTokenizer.tokenWithoutPlus(word); } @@ -611,16 +617,24 @@ public static boolean satisfiesQuery(String s, String[] words) { return true; } + /** + * This looks for a word that is surrounded by non-letter characters. + * This works well only if the word is not a multiword {@link QueryTokenizer#isMultiwordToken(String)}. + * @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); if (pos == -1) return -1; - // pos bukan -1 + // pos is not -1 - // cek kiri + // check left // [pos] [charat pos-1] // 0 * ok // >0 alpha ng @@ -630,7 +644,7 @@ private static int indexOfWholeWord(String text, String word, int start) { continue; } - // cek kanan + // check right int end = pos + word.length(); // [end] [charat end] // len * ok @@ -641,7 +655,7 @@ private static int indexOfWholeWord(String text, String word, int start) { continue; } - // lulus + // passed return pos; } } From fa5503ba196204bd63086490b5b56c115b68edbe Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 7 Jan 2016 14:45:18 +0800 Subject: [PATCH 16/25] Search optimization: do not advance search by 1 character, but consider the length of the searched word, and then advanced by the length of the word --- .../java/yuku/alkitab/base/util/SearchEngine.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 f0e0ee192..4ef14d842 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -248,11 +248,20 @@ private static void searchByGrepForOneChapter(final Version version, final Book int lastV = -1; // Initial search + int consumedLength; + String[] multiword = null; int posWord; if (hasPlus) { + if (QueryTokenizer.isMultiwordToken(word)) { + final List tokenList = QueryTokenizer.tokenizeMultiwordToken(word); + multiword = tokenList.toArray(new String[tokenList.size()]); + } + posWord = indexOfWholeWord(oneChapter, word, 0); + consumedLength = word.length(); } else { posWord = oneChapter.indexOf(word, 0); + consumedLength = word.length(); } if (posWord == -1) { @@ -275,9 +284,11 @@ private static void searchByGrepForOneChapter(final Version version, final Book lastV = verse_0; } if (hasPlus) { - posWord = indexOfWholeWord(oneChapter, word, posWord+1); + posWord = indexOfWholeWord(oneChapter, word, posWord + consumedLength); + consumedLength = word.length(); } else { - posWord = oneChapter.indexOf(word, posWord + 1); + posWord = oneChapter.indexOf(word, posWord + consumedLength); + consumedLength = word.length(); } if (posWord == -1) { return; From 8763bd681ebb8a00db1e870e3ffa690e20faa298 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 7 Jan 2016 16:49:56 +0800 Subject: [PATCH 17/25] Enhancement to multiword search. Multiword search like "God said let" will match "God said, \"Let" or across formatting tags, etc. Highlighting is still not working properly. Search by revindex, also not working properly with multiwords. --- .../alkitab/base/util/QueryTokenizer.java | 4 +- .../yuku/alkitab/base/util/SearchEngine.java | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java b/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java index e8f69085e..947a52a34 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java @@ -96,14 +96,14 @@ static boolean isMultiwordToken(String token) { } 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))) { + if (! (Character.isLetterOrDigit(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" 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 4ef14d842..494b47d96 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -248,17 +248,28 @@ private static void searchByGrepForOneChapter(final Version version, final Book int lastV = -1; // Initial search - int consumedLength; String[] multiword = null; + final int[] consumedLengthPtr = {0}; + + // posWord is the last found position of the searched word + // consumedLength is how much characters in the oneChapter was consumed when searching for the word. + // Both of these variables must be set together in all cases. int posWord; + int consumedLength; + if (hasPlus) { if (QueryTokenizer.isMultiwordToken(word)) { final List tokenList = QueryTokenizer.tokenizeMultiwordToken(word); multiword = tokenList.toArray(new String[tokenList.size()]); } - posWord = indexOfWholeWord(oneChapter, word, 0); - consumedLength = word.length(); + if (multiword != null) { + posWord = indexOfWholeMultiword(oneChapter, multiword, 0, consumedLengthPtr); + consumedLength = consumedLengthPtr[0]; + } else { + posWord = indexOfWholeWord(oneChapter, word, 0); + consumedLength = word.length(); + } } else { posWord = oneChapter.indexOf(word, 0); consumedLength = word.length(); @@ -284,8 +295,13 @@ private static void searchByGrepForOneChapter(final Version version, final Book lastV = verse_0; } if (hasPlus) { - posWord = indexOfWholeWord(oneChapter, word, posWord + consumedLength); - consumedLength = word.length(); + if (multiword != null) { + posWord = indexOfWholeMultiword(oneChapter, multiword, posWord + consumedLength, consumedLengthPtr); + consumedLength = consumedLengthPtr[0]; + } else { + posWord = indexOfWholeWord(oneChapter, word, posWord + consumedLength); + consumedLength = word.length(); + } } else { posWord = oneChapter.indexOf(word, posWord + consumedLength); consumedLength = word.length(); @@ -629,7 +645,7 @@ public static boolean satisfiesQuery(String s, String[] words) { } /** - * This looks for a word that is surrounded by non-letter characters. + * 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 {@link QueryTokenizer#isMultiwordToken(String)}. * @param text haystack * @param word needle @@ -650,7 +666,7 @@ private static int indexOfWholeWord(String text, String word, int start) { // 0 * ok // >0 alpha ng // >0 !alpha ok - if (pos != 0 && Character.isLetter(text.charAt(pos - 1))) { + if (pos != 0 && Character.isLetterOrDigit(text.charAt(pos - 1))) { start = pos + 1; continue; } @@ -661,7 +677,7 @@ private static int indexOfWholeWord(String text, String word, int start) { // len * ok // != len alpha ng // != len !alpha ok - if (end != len && Character.isLetter(text.charAt(end))) { + if (end != len && Character.isLetterOrDigit(text.charAt(end))) { start = pos + 1; continue; } @@ -671,6 +687,93 @@ private static int indexOfWholeWord(String text, String word, int start) { } } + /** + * 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 with '\n' as delimiter between verses. multiword cannot be searched across different verses. + * @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 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, 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 + 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 (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(); + } + + // all words are found! + consumedLengthPtr[0] = pos - firstPos; + return firstPos; + } + } + public static SpannableStringBuilder hilite(final CharSequence s, String[] words, int hiliteColor) { final SpannableStringBuilder res = new SpannableStringBuilder(s); From 851f7aec33064ef6a02e42ce77d0c6253e8ff242 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 7 Jan 2016 17:25:39 +0800 Subject: [PATCH 18/25] Multiword (phrase) search now works with revindex too! --- .../alkitab/base/util/QueryTokenizer.java | 8 +-- .../yuku/alkitab/base/util/SearchEngine.java | 72 ++++++++++--------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java b/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java index 947a52a34..f5dede00a 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java @@ -106,14 +106,14 @@ static boolean isMultiwordToken(String token) { 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" */ - static List tokenizeMultiwordToken(String token) { - List res = new ArrayList<>(); + static String[] tokenizeMultiwordToken(String token) { + final List res = new ArrayList<>(); Matcher m = pattern_letters.matcher(token); while (m.find()) { res.add(m.group()); } - return res; + 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 494b47d96..fb03e725a 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -259,8 +259,7 @@ private static void searchByGrepForOneChapter(final Version version, final Book if (hasPlus) { if (QueryTokenizer.isMultiwordToken(word)) { - final List tokenList = QueryTokenizer.tokenizeMultiwordToken(word); - multiword = tokenList.toArray(new String[tokenList.size()]); + multiword = QueryTokenizer.tokenizeMultiwordToken(word); } if (multiword != null) { @@ -454,42 +453,43 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q // 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()]; + final IntArrayList res2 = new IntArrayList(res.size()); + // later on we will need each multiword to be separated into tokens + final String[][] multiwords_tokens = new String[multiwords.size()][]; + final int[] consumedLengthPtr = {0}; + 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); + multiwords_tokens[i] = QueryTokenizer.tokenizeMultiwordToken(multiwords.get(i)); } 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); + final int ari = res.get(i); - int ariCv = Ari.toBookChapter(ari); + 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; } } + + if (loadedChapter == null) { + continue; + } - int verse_1 = Ari.toVerse(ari); + final int verse_1 = Ari.toVerse(ari); if (verse_1 >= 1 && verse_1 <= loadedChapter.getVerseCount()) { 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_tokens) { + if (indexOfWholeMultiword(text, multiword_tokens, 0, consumedLengthPtr) == -1) { passed = false; break; } @@ -656,29 +656,33 @@ private static int indexOfWholeWord(String text, String word, int start) { 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 is not -1 - // check left - // [pos] [charat pos-1] - // 0 * ok - // >0 alpha ng - // >0 !alpha ok + // [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))) { - start = pos + 1; - continue; + 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; + } } // check right int end = pos + word.length(); - // [end] [charat end] - // len * ok - // != len alpha ng - // != len !alpha ok + // [end] [charat end] + // len * ok + // != len alnum ng + // != len not alnum ok if (end != len && Character.isLetterOrDigit(text.charAt(end))) { - start = pos + 1; + start = pos + 1; // give up continue; } From 514163a035fa4440e78f8bfbbc11c80489e413af Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Mon, 11 Jan 2016 15:56:06 +0800 Subject: [PATCH 19/25] Update btx-karo version_config --- Alkitab/src/main/assets/version_config.json | 4 ++-- .../org.sabda.alkitab-market/version_config.json | 8 ++++---- .../org.sabda.online-market/version_config.json | 2 +- ybuild/overlay/yuku.alkitab-market/version_config.json | 10 +++++----- .../yuku.alkitab.kjv-market/version_config.json | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) 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/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", From 1860cc01e50d788711dac1238c141059d15b847c Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Mon, 11 Jan 2016 17:38:37 +0800 Subject: [PATCH 20/25] Null check on loadChapterText in case of invalid reference link on notes --- .../src/main/java/yuku/alkitab/base/IsiActivity.java | 2 +- .../src/main/java/yuku/alkitab/base/cp/Provider.java | 8 ++++++-- .../main/java/yuku/alkitab/base/model/VersionImpl.java | 10 ++++++++-- .../src/main/java/yuku/alkitab/model/Version.java | 2 ++ 4 files changed, 17 insertions(+), 5 deletions(-) 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/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/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/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); /** From e1f66e8461f94fb7df49af982ef9c698b319be73 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Mon, 11 Jan 2016 17:42:12 +0800 Subject: [PATCH 21/25] DesktopVerseFinder verse detection changes. Cannot have newlines in the middle of a reference. Removed "the" from the word that can't follow a verse reference. --- .../util/DesktopVerseFinder.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 From 1dd1c2751a452ddbe5e320c3391c0f383a7d8a8d Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Wed, 13 Jan 2016 23:36:28 +0800 Subject: [PATCH 22/25] Search engine highlighting matched words now works with multiwords (phrases) --- .../yuku/alkitab/base/util/SearchEngine.java | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) 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 fb03e725a..179fb7a62 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -778,28 +778,32 @@ private static int indexOfWholeMultiword(String text, String[] multiword, int st } } - public static SpannableStringBuilder hilite(final CharSequence s, String[] words, int hiliteColor) { + public static SpannableStringBuilder hilite(final CharSequence s, String[] tokens, int hiliteColor) { final SpannableStringBuilder res = new SpannableStringBuilder(s); - if (words == null) { + if (tokens == null) { return res; } - int word_count = words.length; - boolean[] hasPlusses = new boolean[word_count]; + final int token_count = tokens.length; + final boolean[] hasPlusses = new boolean[token_count]; + final String[][] multiwords_tokens = new String[token_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]); + final String[] tokens2 = new String[token_count]; + System.arraycopy(tokens, 0, tokens2, 0, token_count); + for (int i = 0; i < token_count; i++) { + if (QueryTokenizer.isPlussedToken(tokens2[i])) { + tokens2[i] = QueryTokenizer.tokenWithoutPlus(tokens2[i]); hasPlusses[i] = true; + if (QueryTokenizer.isMultiwordToken(tokens2[i])) { + multiwords_tokens[i] = QueryTokenizer.tokenizeMultiwordToken(tokens2[i]); + } } } - words = words2; + tokens = tokens2; } - // produce a plain text lowercased + // 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); @@ -812,38 +816,48 @@ 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]; + + // 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, 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; + int mintokenindex = -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; + 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; From 619c63b7ce3e7f5ea204d441ea64c1aa68e078f6 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Thu, 14 Jan 2016 01:21:00 +0800 Subject: [PATCH 23/25] Support searching by multiwords in marker list too. Revamped tokenizer and introduced ReadyTokens class that contains preprocessed tokens to avoid multiword-tokenizing multiple times. --- .../alkitab/base/ac/MarkerListActivity.java | 42 ++- .../yuku/alkitab/base/ac/SearchActivity.java | 8 +- .../alkitab/base/util/QueryTokenizer.java | 94 +++--- .../yuku/alkitab/base/util/SearchEngine.java | 289 +++++++++--------- 4 files changed, 219 insertions(+), 214 deletions(-) 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/SearchActivity.java b/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java index dff9c817a..edf9d17af 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/ac/SearchActivity.java @@ -851,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 @@ -901,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/util/QueryTokenizer.java b/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java index f5dede00a..760a893fb 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/QueryTokenizer.java @@ -1,5 +1,8 @@ package yuku.alkitab.base.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -27,53 +30,63 @@ public class QueryTokenizer { * * For compatibility, this also accepts + sign at the beginning of a word or quoted phrase to indicate whole-word matches. * - * @return list of tokens, starting with the character '+' if it is to be matched in a whole-word/whole-phrase manner. + * @return List of tokens, starting with the character '+' if it is to be matched in a whole-word/whole-phrase manner. + * No tokens will be an empty string or "+" (just a plus sign). After the optional '+', there will not be another '+'. */ public static String[] tokenize(String query) { - List 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.isLetterOrDigit(c) || ((c=='-' || c=='\'') && i > start && i < len-1))) { - return true; - } - } - return false; - } - static Pattern pattern_letters = Pattern.compile("[\\p{javaLetterOrDigit}'-]+"); /** * 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 String[] tokenizeMultiwordToken(String token) { - final 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()); } + + 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 179fb7a62..f2f7c0670 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 { @@ -68,15 +68,52 @@ 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); + String[] tokens = QueryTokenizer.tokenize(query.query_string); // sort by word length, then alphabetically - Arrays.sort(words, (object1, object2) -> { + Arrays.sort(tokens, (object1, object2) -> { final int len1 = object1.length(); final int len2 = object2.length(); @@ -89,28 +126,28 @@ public static IntArrayList searchByGrep(final Version version, final Query query // 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; - for (final String word : words) { + for (final String token : tokens) { final 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"); + result = searchByGrepInside(version, token, prev, query.bookIds); + Log.d(TAG, "search token '" + token + "' needed: " + (System.currentTimeMillis() - ms) + " ms"); } if (prev != null) { @@ -182,12 +219,12 @@ 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(); - final boolean hasPlus = QueryTokenizer.isPlussedToken(word); + final boolean hasPlus = QueryTokenizer.isPlussedToken(token); if (hasPlus) { - word = QueryTokenizer.tokenWithoutPlus(word); + token = QueryTokenizer.tokenWithoutPlus(token); } if (source == null) { @@ -199,7 +236,7 @@ static IntArrayList searchByGrepInside(final Version version, String word, final for (int chapter_1 = 1; chapter_1 <= book.chapter_count; chapter_1++) { // try to find it wholly in a chapter final int ariBc = Ari.encode(book.bookId, chapter_1, 0); - searchByGrepForOneChapter(version, book, chapter_1, word, hasPlus, ariBc, res); + searchByGrepForOneChapter(version, book, chapter_1, token, hasPlus, ariBc, res); } if (BuildConfig.DEBUG) Log.d(TAG, "searchByGrepInside book " + book.shortName + " done. res.size = " + res.size()); @@ -220,7 +257,7 @@ static IntArrayList searchByGrepInside(final Version version, String word, final final Book book = version.getBook(Ari.toBook(curAriBc)); final int chapter_1 = Ari.toChapter(curAriBc); - searchByGrepForOneChapter(version, book, chapter_1, word, hasPlus, curAriBc, res); + searchByGrepForOneChapter(version, book, chapter_1, token, hasPlus, curAriBc, res); count++; } @@ -232,12 +269,12 @@ static IntArrayList searchByGrepInside(final Version version, String word, final } /** - * @param word searched word without plusses + * @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 word had plus + * @param hasPlus whether the token had plus */ - private static void searchByGrepForOneChapter(final Version version, final Book book, final int chapter_1, final String word, final boolean hasPlus, final int ariBc, final IntArrayList res) { + 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) { @@ -251,38 +288,36 @@ private static void searchByGrepForOneChapter(final Version version, final Book String[] multiword = null; final int[] consumedLengthPtr = {0}; - // posWord is the last found position of the searched word - // consumedLength is how much characters in the oneChapter was consumed when searching for the word. + // 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 posWord; + int posToken; int consumedLength; if (hasPlus) { - if (QueryTokenizer.isMultiwordToken(word)) { - multiword = QueryTokenizer.tokenizeMultiwordToken(word); - } + multiword = QueryTokenizer.tokenizeMultiwordToken(token); if (multiword != null) { - posWord = indexOfWholeMultiword(oneChapter, multiword, 0, consumedLengthPtr); + posToken = indexOfWholeMultiword(oneChapter, multiword, 0, consumedLengthPtr); consumedLength = consumedLengthPtr[0]; } else { - posWord = indexOfWholeWord(oneChapter, word, 0); - consumedLength = word.length(); + posToken = indexOfWholeWord(oneChapter, token, 0); + consumedLength = token.length(); } } else { - posWord = oneChapter.indexOf(word, 0); - consumedLength = word.length(); + posToken = oneChapter.indexOf(token, 0); + consumedLength = token.length(); } - if (posWord == -1) { - // initial search does not return results. It means the whole chapter does not contain the word. + 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); if (posN == -1) { @@ -295,17 +330,17 @@ private static void searchByGrepForOneChapter(final Version version, final Book } if (hasPlus) { if (multiword != null) { - posWord = indexOfWholeMultiword(oneChapter, multiword, posWord + consumedLength, consumedLengthPtr); + posToken = indexOfWholeMultiword(oneChapter, multiword, posToken + consumedLength, consumedLengthPtr); consumedLength = consumedLengthPtr[0]; } else { - posWord = indexOfWholeWord(oneChapter, word, posWord + consumedLength); - consumedLength = word.length(); + posToken = indexOfWholeWord(oneChapter, token, posToken + consumedLength); + consumedLength = token.length(); } } else { - posWord = oneChapter.indexOf(word, posWord + consumedLength); - consumedLength = word.length(); + posToken = oneChapter.indexOf(token, posToken + consumedLength); + consumedLength = token.length(); } - if (posWord == -1) { + if (posToken == -1) { return; } } @@ -330,55 +365,21 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q 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 @@ -394,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; @@ -410,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"); } @@ -452,17 +458,18 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q // 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) { + 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()); - // later on we will need each multiword to be separated into tokens - final String[][] multiwords_tokens = new String[multiwords.size()][]; final int[] consumedLengthPtr = {0}; - for (int i = 0, len = multiwords.size(); i < len; i++) { - multiwords_tokens[i] = QueryTokenizer.tokenizeMultiwordToken(multiwords.get(i)); - } - 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++) { @@ -488,7 +495,7 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q String text = loadedChapter.getVerse(verse_1 - 1); if (text != null) { boolean passed = true; - for (final String[] multiword_tokens : multiwords_tokens) { + for (final String[] multiword_tokens : multiwords) { if (indexOfWholeMultiword(text, multiword_tokens, 0, consumedLengthPtr) == -1) { passed = false; break; @@ -622,21 +629,24 @@ private static RevIndex loadRevIndex() { } /** - * 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) { - final boolean hasPlus = QueryTokenizer.isPlussedToken(word); - if (hasPlus) { - 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 wordPos; if (hasPlus) { - wordPos = indexOfWholeWord(s, word, 0); + final String[] multiword_tokens = rt.multiwords_tokens[i]; + if (multiword_tokens != null) { + wordPos = indexOfWholeMultiword(s, multiword_tokens, 0, null); + } else { + wordPos = indexOfWholeWord(s, rt.tokens[i], 0); + } } else { - wordPos = s.indexOf(word); + wordPos = s.indexOf(rt.tokens[i]); } + if (wordPos == -1) { return false; } @@ -646,7 +656,7 @@ public static boolean satisfiesQuery(String s, String[] words) { /** * 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 {@link QueryTokenizer#isMultiwordToken(String)}. + * This works well only if the word is not a multiword. * @param text haystack * @param word needle * @param start start at character @@ -700,7 +710,7 @@ private static int indexOfWholeWord(String text, String word, int start) { * @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, int[] consumedLengthPtr) { + private static int indexOfWholeMultiword(String text, String[] multiword, int start, @Nullable int[] consumedLengthPtr) { final int len = text.length(); final String firstWord = multiword[0]; @@ -708,7 +718,7 @@ private static int indexOfWholeMultiword(String text, String[] multiword, int st final int firstPos = indexOfWholeWord(text, firstWord, start); if (firstPos == -1) { // not even the first word is found, so we give up - consumedLengthPtr[0] = 0; + if (consumedLengthPtr != null) consumedLengthPtr[0] = 0; return -1; } @@ -773,35 +783,19 @@ private static int indexOfWholeMultiword(String text, String[] multiword, int st } // all words are found! - consumedLengthPtr[0] = pos - firstPos; + if (consumedLengthPtr != null) consumedLengthPtr[0] = pos - firstPos; return firstPos; } } - public static SpannableStringBuilder hilite(final CharSequence s, String[] tokens, int hiliteColor) { + public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTokens rt, int hiliteColor) { final SpannableStringBuilder res = new SpannableStringBuilder(s); - if (tokens == null) { + if (rt == null) { return res; } - - final int token_count = tokens.length; - final boolean[] hasPlusses = new boolean[token_count]; - final String[][] multiwords_tokens = new String[token_count][]; - { // point to copy - final String[] tokens2 = new String[token_count]; - System.arraycopy(tokens, 0, tokens2, 0, token_count); - for (int i = 0; i < token_count; i++) { - if (QueryTokenizer.isPlussedToken(tokens2[i])) { - tokens2[i] = QueryTokenizer.tokenWithoutPlus(tokens2[i]); - hasPlusses[i] = true; - if (QueryTokenizer.isMultiwordToken(tokens2[i])) { - multiwords_tokens[i] = QueryTokenizer.tokenizeMultiwordToken(tokens2[i]); - } - } - } - tokens = tokens2; - } + + final int token_count = rt.token_count; // from source text, produce a plain text lowercased final char[] newString = new char[s.length()]; @@ -819,6 +813,11 @@ public static SpannableStringBuilder hilite(final CharSequence s, String[] token 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) { From e24a507ceb973d282541193ed9e5a930a72cffff Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Fri, 15 Jan 2016 02:19:04 +0800 Subject: [PATCH 24/25] Highlight correctly multiwords that are contained in a 'rendered' verse with possible newline characters. --- .../yuku/alkitab/base/util/SearchEngine.java | 139 +++++++++--------- 1 file changed, 70 insertions(+), 69 deletions(-) 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 f2f7c0670..22a4a3bb9 100644 --- a/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java +++ b/Alkitab/src/main/java/yuku/alkitab/base/util/SearchEngine.java @@ -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,7 +62,7 @@ public static class Query implements Parcelable { } }; } - + static class RevIndex extends HashMap { public RevIndex() { super(32768); @@ -105,13 +105,13 @@ public ReadyTokens(final String[] tokens) { } } } - + private static SoftReference cache_revIndex; private static Semaphore revIndexLoading = new Semaphore(1); - + public static IntArrayList searchByGrep(final Version version, final Query query) { String[] tokens = QueryTokenizer.tokenize(query.query_string); - + // sort by word length, then alphabetically Arrays.sort(tokens, (object1, object2) -> { final int len1 = object1.length(); @@ -123,7 +123,7 @@ public static IntArrayList searchByGrep(final Version version, final Query query } return 1; }); - + // remove duplicates { final ArrayList atokens = new ArrayList<>(); @@ -137,7 +137,7 @@ public static IntArrayList searchByGrep(final Version version, final Query query tokens = atokens.toArray(new String[atokens.size()]); if (BuildConfig.DEBUG) Log.d(TAG, "tokens = " + Arrays.toString(tokens)); } - + // really search IntArrayList result = null; @@ -162,22 +162,22 @@ public static IntArrayList searchByGrep(final Version version, final Query query 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++; @@ -188,7 +188,7 @@ private static IntArrayList intersect(IntArrayList a, IntArrayList b) { apos++; } } - + return res; } @@ -200,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++; @@ -238,33 +238,33 @@ static IntArrayList searchByGrepInside(final Version version, String token, fina 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; // 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. final Book book = version.getBook(Ari.toBook(curAriBc)); 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; } @@ -298,7 +298,7 @@ private static void searchByGrepForOneChapter(final Version version, final Book multiword = QueryTokenizer.tokenizeMultiwordToken(token); if (multiword != null) { - posToken = indexOfWholeMultiword(oneChapter, multiword, 0, consumedLengthPtr); + posToken = indexOfWholeMultiword(oneChapter, multiword, 0, true, consumedLengthPtr); consumedLength = consumedLengthPtr[0]; } else { posToken = indexOfWholeWord(oneChapter, token, 0); @@ -330,7 +330,7 @@ private static void searchByGrepForOneChapter(final Version version, final Book } if (hasPlus) { if (multiword != null) { - posToken = indexOfWholeMultiword(oneChapter, multiword, posToken + consumedLength, consumedLengthPtr); + posToken = indexOfWholeMultiword(oneChapter, multiword, posToken + consumedLength, true, consumedLengthPtr); consumedLength = consumedLengthPtr[0]; } else { posToken = indexOfWholeWord(oneChapter, token, posToken + consumedLength); @@ -361,7 +361,7 @@ 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); @@ -381,7 +381,7 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } timing.addSplit("Tokenize query"); - + // optimization, if user doesn't filter any books boolean wholeBibleSearched = true; boolean[] searchedBookIds = new boolean[66]; @@ -455,7 +455,7 @@ 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 final List multiwords = new ArrayList<>(); @@ -467,14 +467,14 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q 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++) { 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 final Book book = version.getBook(Ari.toBook(ari)); @@ -489,14 +489,14 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q 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 (final String[] multiword_tokens : multiwords) { - if (indexOfWholeMultiword(text, multiword_tokens, 0, consumedLengthPtr) == -1) { + if (indexOfWholeMultiword(text, multiword_tokens, 0, false, consumedLengthPtr) == -1) { passed = false; break; } @@ -507,9 +507,9 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q } } } - + res = res2; - + timing.addSplit("filter for multiword tokens (" + res.size() + ")"); } @@ -517,7 +517,7 @@ public static IntArrayList searchByRevIndex(final Version version, final Query q return res; } - + public static void preloadRevIndex() { new Thread() { @Override public void run() { @@ -533,11 +533,11 @@ public static void preloadRevIndex() { } }.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 * { @@ -549,14 +549,14 @@ public static void preloadRevIndex() { * 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() { @@ -577,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]; @@ -609,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; @@ -620,10 +620,10 @@ private static RevIndex loadRevIndex() { } br.close(); - } catch (IOException e) { + } catch (IOException e) { return null; } - + cache_revIndex = new SoftReference<>(res); return res; } @@ -635,19 +635,19 @@ public static boolean satisfiesTokens(final String s, @NonNull final ReadyTokens for (int i = 0; i < rt.token_count; i++) { final boolean hasPlus = rt.hasPlusses[i]; - final int wordPos; + final int posToken; if (hasPlus) { final String[] multiword_tokens = rt.multiwords_tokens[i]; if (multiword_tokens != null) { - wordPos = indexOfWholeMultiword(s, multiword_tokens, 0, null); + posToken = indexOfWholeMultiword(s, multiword_tokens, 0, false, null); } else { - wordPos = indexOfWholeWord(s, rt.tokens[i], 0); + posToken = indexOfWholeWord(s, rt.tokens[i], 0); } } else { - wordPos = s.indexOf(rt.tokens[i]); + posToken = s.indexOf(rt.tokens[i]); } - if (wordPos == -1) { + if (posToken == -1) { return false; } } @@ -664,11 +664,11 @@ public static boolean satisfiesTokens(final String s, @NonNull final ReadyTokens */ private static int indexOfWholeWord(String text, String word, int start) { final int len = text.length(); - + while (true) { final int pos = text.indexOf(word, start); if (pos == -1) return -1; - + // check left // [pos] [charat pos-1] [charat pos-2] // 0 ok @@ -684,7 +684,7 @@ private static int indexOfWholeWord(String text, String word, int start) { continue; } } - + // check right int end = pos + word.length(); // [end] [charat end] @@ -695,7 +695,7 @@ private static int indexOfWholeWord(String text, String word, int start) { start = pos + 1; // give up continue; } - + // passed return pos; } @@ -704,13 +704,14 @@ private static int indexOfWholeWord(String text, String word, int start) { /** * 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 with '\n' as delimiter between verses. multiword cannot be searched across different verses. + * @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, @Nullable int[] consumedLengthPtr) { + 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]; @@ -750,7 +751,7 @@ private static int indexOfWholeMultiword(String text, String[] multiword, int st } } else if (Character.isLetterOrDigit(c)) { break; - } else if (c == '\n') { + } else if (isNewlineDelimitedText && c == '\n') { // can't cross verse boundary, so we give up and try from beginning again start = pos + 1; continue findAllWords; @@ -790,7 +791,7 @@ private static int indexOfWholeMultiword(String text, String[] multiword, int st public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTokens rt, int hiliteColor) { final SpannableStringBuilder res = new SpannableStringBuilder(s); - + if (rt == null) { return res; } @@ -808,7 +809,7 @@ public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTok } } final String plainText = new String(newString); - + int pos = 0; final int[] attempts = new int[token_count]; final int[] consumedLengths = new int[token_count]; @@ -824,7 +825,7 @@ public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTok for (int i = 0; i < token_count; i++) { if (hasPlusses[i]) { if (multiwords_tokens[i] != null) { - attempts[i] = indexOfWholeMultiword(plainText, multiwords_tokens[i], pos, consumedLengthPtr); + attempts[i] = indexOfWholeMultiword(plainText, multiwords_tokens[i], pos, false, consumedLengthPtr); consumedLengths[i] = consumedLengthPtr[0]; } else { attempts[i] = indexOfWholeWord(plainText, tokens[i], pos); @@ -839,7 +840,7 @@ public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTok // from the attempts above, find the earliest int minpos = Integer.MAX_VALUE; 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) { @@ -848,17 +849,17 @@ public static SpannableStringBuilder hilite(final CharSequence s, final ReadyTok } } } - + if (mintokenindex == -1) { break; // no more } - + 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; } } From 74c2c3c6ef8f69f774a4f5c07c7a5e9832b49e96 Mon Sep 17 00:00:00 2001 From: Yuku on yuku8 Date: Fri, 15 Jan 2016 02:21:35 +0800 Subject: [PATCH 25/25] Bump version --- Alkitab/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 {