diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7c3a5656f4..9ea278697c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -119,6 +119,11 @@
android:configChanges="keyboardHidden|orientation|screenSize|locale"
android:label="@string/search"
/>
+ = cleanSearchString.split(" ")
+ for (it in wordArray) {
+ var decoratedWord = it
+ if (includeAllEndings) decoratedWord += "* "
+ if (isStrongsSearch) decoratedWord = "strong:$strongs$decoratedWord "
+ if (fuzzySearchAccuracy != null) {
+ val fuzzySearchAccuracyAdjusted = if (fuzzySearchAccuracy.equals(1.0)) 0.99 else fuzzySearchAccuracy
+ decoratedWord += "~%.2f ".format(fuzzySearchAccuracyAdjusted)
+ }
+ newSearchString += decoratedWord
+ }
+ cleanSearchString = newSearchString.trim()
+ }
+
+ if (proximityWords != null) {
+ cleanSearchString = "\"$cleanSearchString\"~$proximityWords"
+ }
// add search type (all/any/phrase) to search string
- var decorated: String = searchType.decorate(cleanSearchString)
+ var decorated = searchType.decorate(cleanSearchString)
originalSearchString = decorated
// add bible section limitation to search text
@@ -139,6 +167,43 @@ class SearchControl @Inject constructor(
return searchResults
}
+ fun getSearchResultVerseText(key: Key?): String {
+ // There is similar functionality in BookmarkControl
+ // This is much slower than 'getSearchResultVerseElement'. Why? In the old version this was VERY fast.
+ var verseText = ""
+ try {
+ val doc = windowControl.activeWindowPageManager.currentPage.currentDocument
+ val cat = doc!!.bookCategory
+ verseText = if (cat == BookCategory.BIBLE || cat == BookCategory.COMMENTARY) {
+ getPlainText(doc, key)
+ } else {
+ val bible = windowControl.activeWindowPageManager.currentBible.currentDocument!!
+ getPlainText(bible, key)
+ }
+ verseText = limitTextLength(verseText)!!
+ } catch (e: Exception) {
+ Log.e(TAG, "Error getting verse text", e)
+ }
+ return verseText
+ }
+
+ fun getSearchResultVerseElement(key: Key?): Element {
+ // There is similar functionality in BookmarkControl
+ var xmlVerse:Element? = null
+ try {
+ val doc = windowControl.activeWindowPageManager.currentPage.currentDocument
+ val cat = doc!!.bookCategory
+ xmlVerse = if (cat == BookCategory.BIBLE || cat == BookCategory.COMMENTARY) {
+ readOsisFragment(doc, key)
+ } else {
+ val bible = windowControl.activeWindowPageManager.currentBible.currentDocument!!
+ readOsisFragment(bible, key)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error getting verse text", e)
+ }
+ return xmlVerse!!
+ }
/** double spaces, :, and leading or trailing space cause lucene errors
*/
private fun cleanSearchString(search: String): String {
@@ -212,7 +277,7 @@ class SearchControl @Inject constructor(
const val TARGET_DOCUMENT = "TargetDocument"
private const val STRONG_COLON_STRING = LuceneIndex.FIELD_STRONG + ":"
private const val STRONG_COLON_STRING_PLACE_HOLDER = LuceneIndex.FIELD_STRONG + "COLON"
- const val MAX_SEARCH_RESULTS = 1000
+ const val MAX_SEARCH_RESULTS = 10000
private const val TAG = "SearchControl"
}
}
diff --git a/app/src/main/java/net/bible/android/view/activity/ActivityComponent.kt b/app/src/main/java/net/bible/android/view/activity/ActivityComponent.kt
index 2c39ff31bb..0e1207f80e 100644
--- a/app/src/main/java/net/bible/android/view/activity/ActivityComponent.kt
+++ b/app/src/main/java/net/bible/android/view/activity/ActivityComponent.kt
@@ -41,6 +41,7 @@ import net.bible.android.view.activity.page.screen.DocumentViewManager
import net.bible.android.view.activity.readingplan.DailyReading
import net.bible.android.view.activity.readingplan.DailyReadingList
import net.bible.android.view.activity.readingplan.ReadingPlanSelectorList
+import net.bible.android.view.activity.search.MySearchResults
import net.bible.android.view.activity.search.Search
import net.bible.android.view.activity.search.SearchIndex
import net.bible.android.view.activity.search.SearchIndexProgressStatus
@@ -102,6 +103,7 @@ interface ActivityComponent {
fun inject(w: SpeakTransportWidget)
fun inject(search: Search)
fun inject(searchResults: SearchResults)
+ fun inject(mySearchResults: MySearchResults)
fun inject(history: History)
// Services
diff --git a/app/src/main/java/net/bible/android/view/activity/navigation/GridChoosePassageBook.kt b/app/src/main/java/net/bible/android/view/activity/navigation/GridChoosePassageBook.kt
index ed347d650d..26b001317f 100644
--- a/app/src/main/java/net/bible/android/view/activity/navigation/GridChoosePassageBook.kt
+++ b/app/src/main/java/net/bible/android/view/activity/navigation/GridChoosePassageBook.kt
@@ -320,10 +320,10 @@ class GridChoosePassageBook : CustomTitlebarActivityBase(R.menu.choose_passage_b
private val HISTORY_COLOR = Color.rgb(0xFE, 0xCC, 0x9B)
private val WISDOM_COLOR = Color.rgb(0x99, 0xFF, 0x99)
private val MAJOR_PROPHETS_COLOR = Color.rgb(0xFF, 0x99, 0xFF)
- private val MINOR_PROPHETS_COLOR = Color.rgb(0xFF, 0xFE, 0xCD)
+ private val MINOR_PROPHETS_COLOR = Color.rgb(0xE6, 0xE5, 0xB8)
private val GOSPEL_COLOR = Color.rgb(0xFF, 0x97, 0x03)
private val ACTS_COLOR = Color.rgb(0x00, 0x99, 0xFF)
- private val PAULINE_COLOR = Color.rgb(0xFF, 0xFF, 0x31)
+ private val PAULINE_COLOR = Color.rgb(0xF5, 0xf5, 0x21)
private val GENERAL_EPISTLES_COLOR = Color.rgb(0x67, 0xCC, 0x66) // changed 99 to CC to make a little clearer on dark background
private val REVELATION_COLOR = Color.rgb(0xFE, 0x33, 0xFF)
private val OTHER_COLOR = ACTS_COLOR
diff --git a/app/src/main/java/net/bible/android/view/activity/search/MySearchResults.kt b/app/src/main/java/net/bible/android/view/activity/search/MySearchResults.kt
new file mode 100644
index 0000000000..003deb3d0c
--- /dev/null
+++ b/app/src/main/java/net/bible/android/view/activity/search/MySearchResults.kt
@@ -0,0 +1,442 @@
+package net.bible.android.view.activity.search
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Typeface
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import android.util.Log
+import android.view.MenuItem
+import android.view.View
+import android.view.Window
+import android.widget.Toast
+import androidx.core.text.toHtml
+import com.google.android.material.tabs.TabLayout
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentPagerAdapter
+import androidx.viewpager.widget.ViewPager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.bible.android.activity.databinding.SearchResultsStatisticsBinding
+import java.util.ArrayList
+import net.bible.android.activity.R
+import net.bible.android.control.event.ABEventBus
+import net.bible.android.control.event.ToastEvent
+import net.bible.android.control.link.LinkControl
+import net.bible.android.control.navigation.NavigationControl
+import net.bible.android.control.page.window.WindowControl
+import net.bible.android.control.search.SearchControl
+//import net.bible.android.view.activity.search.SearchResultsDto
+import net.bible.android.view.activity.base.CustomTitlebarActivityBase
+import net.bible.android.view.activity.base.Dialogs
+import net.bible.android.view.activity.navigation.GridChoosePassageBook
+import net.bible.android.view.activity.search.searchresultsactionbar.SearchResultsActionBarManager
+import org.apache.commons.lang3.StringUtils
+import org.crosswire.jsword.passage.Key
+import org.crosswire.jsword.passage.Verse
+import javax.inject.Inject
+import org.crosswire.jsword.versification.BibleBook
+import org.crosswire.jsword.versification.Versification
+import net.bible.service.common.CommonUtils
+import net.bible.service.common.CommonUtils.resources
+
+
+
+private var TAB_TITLES = arrayOf(
+ resources.getString(R.string.add_bookmark_whole_verse1),
+ resources.getString(R.string.by_book),
+ resources.getString(R.string.by_word)
+)
+var mCurrentlyDisplayedSearchResults: List = ArrayList() // I tried to make this a non-global but i had the same problem as before where the values in the variable was one behind the search. I don't know why!!
+const val verseTabPosition = 0
+private const val bookTabPosition = 1
+private const val wordTabPosition = 2
+
+class BookStat(val book: String, var count: Int,
+ val bookInitials: String,
+ val bookOrdinal:Int,
+ val listIndex:Int,
+ val color: Int) {
+ override fun toString(): String = "$book: $count"
+}
+class WordStat(var word: String,
+ var verseIndexes: IntArray,
+ val originalWord: String,
+ var tag: Boolean = false) {
+ override fun toString(): String = "$originalWord: ${verseIndexes.count()}"
+}
+
+private var wordHits: ArrayList>> = arrayListOf()
+
+class SearchResultsData : Parcelable {
+ @JvmField var id: Int? // Holds the original index in the results. Used for selection and filtering.
+ @JvmField var osisKey: String?
+ @JvmField var reference: String?
+ @JvmField var translation: String?
+ @JvmField var verse: String?
+ @JvmField var verseHtml: String? // I would prefer to pass a spannable string but i dont know how to make it parcelable
+
+ /* What I really want to do is make the 'Key' parcelable but I don't know how to do that. So instead I have to send the properties I need and get the key later on */
+
+ constructor(id: Int?, osisKey: String?, reference: String?, translation: String?, verseStr: String?, verseHtml: String?) {
+ this.id = id;
+ this.osisKey = osisKey
+ this.reference = reference
+ this.translation = translation
+ this.verse = verseStr
+ this.verseHtml = verseHtml
+ }
+
+ private constructor(p: Parcel) {
+ id = p.readInt()
+ osisKey = p.readString()
+ reference = p.readString()
+ translation = p.readString()
+ verse = p.readString()
+ verseHtml = p.readString()
+ }
+
+ override fun describeContents(): Int {
+ return this.hashCode()
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeInt(id!!)
+ dest.writeString(osisKey)
+ dest.writeString(reference)
+ dest.writeString(translation)
+ dest.writeString(verse)
+ dest.writeString(verseHtml)
+
+ }
+
+ companion object CREATOR: Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): SearchResultsData {
+ return SearchResultsData(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
+
+class MySearchResults : CustomTitlebarActivityBase() {
+ private lateinit var binding: SearchResultsStatisticsBinding
+ private var mSearchResultsHolder: SearchResultsDto? = null
+ private lateinit var sectionsPagerAdapter: SearchResultsPagerAdapter
+ private val mSearchResultsArray = ArrayList()
+ private val bookStatistics = mutableListOf()
+ private val wordStatistics = mutableListOf()
+ private val keyWordStatistics = mutableListOf()
+
+ @Inject lateinit var navigationControl: NavigationControl
+ private var isScriptureResultsCurrentlyShown = true
+ @Inject lateinit var searchResultsActionBarManager: SearchResultsActionBarManager
+ @Inject lateinit var searchControl: SearchControl
+ @Inject lateinit var linkControl: LinkControl
+ @Inject lateinit var windowControl: WindowControl
+
+ private val versification get() = navigationControl.versification
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ @SuppressLint("MissingSuperCall")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.i(TAG, "Displaying Search results view")
+
+ binding = SearchResultsStatisticsBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ buildActivityComponent().inject(this)
+
+ val bundle = Bundle()
+ bundle.putString("edttext", "From Activity")
+
+ sectionsPagerAdapter = SearchResultsPagerAdapter(this,
+ supportFragmentManager, searchControl, windowControl, intent,
+ mSearchResultsArray, bookStatistics, wordStatistics, keyWordStatistics
+ )
+
+ binding.viewPager.run {
+ adapter = sectionsPagerAdapter
+ // The progressbar on the 3rd tab goes to zero when the view is lost.
+ // So just keep it in memory and all is fine. It is not a big view so i think it is ok.
+ offscreenPageLimit = 2
+ binding.tabs.setupWithViewPager(this)
+ }
+
+ searchResultsActionBarManager.registerScriptureToggleClickListener(scriptureToggleClickListener)
+ setActionBarManager(searchResultsActionBarManager)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ isScriptureResultsCurrentlyShown = searchControl.isCurrentlyShowingScripture
+
+ GlobalScope.launch {
+ prepareResults()
+ }
+ }
+
+ private suspend fun prepareResults() = withContext(Dispatchers.Main){
+ binding.loadingIndicator.visibility = View.VISIBLE
+ binding.empty.visibility = View.GONE
+ if (fetchSearchResults()) { // initialise adapters before result population - easier when updating due to later Scripture toggle
+ populateViewResultsAdapter()
+ }
+ binding.loadingIndicator.visibility = View.GONE
+// if(listAdapter?.isEmpty == true) {
+// binding.empty.visibility = View.VISIBLE
+// }
+ }
+
+ private suspend fun fetchSearchResults(): Boolean = withContext(Dispatchers.IO) {
+ Log.i(TAG, "Preparing search results")
+ var isOk: Boolean
+ try { // get search string - passed in using extras so extras cannot be null
+ val extras = intent.extras!!
+ val searchText = extras.getString(SearchControl.SEARCH_TEXT)
+ var searchDocument = extras.getString(SearchControl.SEARCH_DOCUMENT)
+ if (StringUtils.isEmpty(searchDocument)) {
+ searchDocument = windowControl.activeWindowPageManager.currentPage.currentDocument!!.initials
+ }
+ mSearchResultsHolder = searchControl.getSearchResults(searchDocument, searchText)
+ // tell user how many results were returned
+ val msg:String = if (mCurrentlyDisplayedSearchResults.size >= SearchControl.MAX_SEARCH_RESULTS) {
+ getString(R.string.search_showing_first, SearchControl.MAX_SEARCH_RESULTS)
+ } else {
+ getString(R.string.search_result_count, mSearchResultsHolder!!.size)
+ }
+ withContext(Dispatchers.Main) {
+ ABEventBus.post(ToastEvent(msg))
+ }
+ isOk = true
+ } catch (e: Exception) {
+ Log.e(TAG, "Error processing search query", e)
+ isOk = false
+ Dialogs.showErrorMsg(R.string.error_executing_search) { onBackPressed() }
+ }
+ return@withContext isOk
+ }
+
+ /**
+ * Move search results into view Adapter
+ */
+
+ private fun populateViewResultsAdapter() {
+
+ mCurrentlyDisplayedSearchResults = if (isScriptureResultsCurrentlyShown) {
+ mSearchResultsHolder!!.mainSearchResults
+ } else {
+ mSearchResultsHolder!!.otherSearchResults
+ }
+ val extras = intent.extras
+ val searchDocument = extras!!.getString(SearchControl.SEARCH_DOCUMENT)
+
+ mSearchResultsArray.clear()
+ bookStatistics.clear()
+ wordStatistics.clear()
+ keyWordStatistics.clear()
+
+ var listIndex = 0
+ var totalWords = 0
+ val searchHighlight = SearchHighlight(SearchControl.originalSearchString!!)
+ for (key in mCurrentlyDisplayedSearchResults) {
+ // Add verse to results array
+
+ var verseTextSpannable: SpannableString?
+ val verseTextElement = searchControl.getSearchResultVerseElement(key)
+
+ // Build a spannable verse highlighting the relevant words.
+ verseTextSpannable = searchHighlight.generateSpannableFromVerseElement(verseTextElement)
+
+ mSearchResultsArray.add(SearchResultsData(listIndex, key.osisID.toString(), key.name,searchDocument, "text", verseTextSpannable.toHtml()))
+
+ // Add book to the book statistics array
+ val mBibleBook = (key as Verse).book
+ val bookOrdinal = mBibleBook.ordinal
+ val bookNameLong = versification.getLongName(mBibleBook) // key.rootName
+ val bookStat = bookStatistics.firstOrNull { it.book == bookNameLong }
+ if (bookStat == null) {
+ bookStatistics.add(BookStat(bookNameLong, 1, bookNameLong, bookOrdinal, listIndex, GridChoosePassageBook.getBookColorAndGroup(bookOrdinal).Color ))
+ } else {
+ bookStatistics.first { it.book == bookNameLong }.count += 1
+ }
+
+ // Add words in this verse to word statistics array
+ val verseSpans: Array = verseTextSpannable.getSpans(0, verseTextSpannable.length, StyleSpan::class.java)
+ for (i in verseSpans.indices) {
+ if (verseSpans[i].style == Typeface.BOLD) {
+ totalWords += 1
+ val start = verseTextSpannable.getSpanStart(verseSpans[i])
+ val end = verseTextSpannable.getSpanEnd(verseSpans[i])
+
+ val wordArray = CharArray(end-start)
+ verseTextSpannable.getChars(start, end, wordArray, 0)
+ val originalWord = wordArray.joinToString("")
+ val word = originalWord.lowercase()
+ val wordStat = wordStatistics.firstOrNull { it.word == word }
+ if (wordStat == null) {
+ wordStatistics.add(WordStat(word, intArrayOf(listIndex), originalWord))
+ } else {
+ wordStatistics.first { it.word == word }.verseIndexes += listIndex
+ }
+ }
+ }
+ listIndex += 1
+ }
+ // Initialise keyword lists
+ val tmpKeyWordStatistics = mutableListOf()
+ val tmpWordStatistics = wordStatistics.toMutableList()
+// val wordsToExclude = listOf("and","or")
+
+// // Cleanup original word list
+// tmpWordStatistics.filter {"“" in it.originalWord }.map {
+// it.word = it.originalWord.replace("“","",true)
+// }
+
+ // Exclude short words (eg 'and', 'or', 'it' etc)
+ tmpWordStatistics.filter{it.word.length<=3 && it.word != "God"}.map {it.tag = true}
+
+ // Remove short single words from the beginning of a phrase
+ tmpWordStatistics.filter{it.word.indexOf(" ") > 0 && it.word.indexOf(" ") < 4}.map {
+ it.word = it.word.substring(it.word.indexOf(" ")+1).trim()
+ }
+
+ // Find all single words
+ tmpWordStatistics.filter{ !it.tag }.sortedBy { it.originalWord.length }.map { wordStat ->
+ // TODO: I think longer single words that contain shorter single words are being excluded (correctly) but their VerseIndexes are not being included
+ // TODO: Check Greek word Thirsty G1372
+ // TODO: This IF should be able to be moved into the filter above
+ if (" " !in wordStat.word) {
+ wordStat.tag = true // Don't process this entry again
+ if (!keyWordStatistics.any { wordStat.word.contains(it.word,true) }) {
+ tmpKeyWordStatistics.add(WordStat(wordStat.word, wordStat.verseIndexes, wordStat.originalWord))
+ keyWordStatistics.add(WordStat(wordStat.word, wordStat.verseIndexes, wordStat.word))
+ }
+ }
+ }
+
+ // Find phrases that have a single keyword in them. Order is important to exclude words with punctuation (eg "Gather)
+ keyWordStatistics.sortedBy { it.originalWord.length }.map { keyWord ->
+ tmpWordStatistics.filter { !it.tag && it.word.contains(keyWord.word,true) }.map { multiWordStat ->
+ multiWordStat.tag = true // Exclude the
+ keyWord.verseIndexes += multiWordStat.verseIndexes
+ }
+ }
+ // Find phrases that are in other longer phrases
+ tmpWordStatistics.filter{ !it.tag }.sortedBy { it.word.length }.map { shortPhrase ->
+ tmpWordStatistics.filter { !it.tag && it.word != shortPhrase.word && it.word.contains(shortPhrase.word,true) }
+ .map { longPhrase ->
+ longPhrase.tag = true
+ shortPhrase.verseIndexes += longPhrase.verseIndexes
+ }
+ }
+ // Add all the phrases that have not been tagged (excludes single words, long phrases that appear in shorter phrases)
+ tmpWordStatistics.filter{ !it.tag }.map {
+ keyWordStatistics.add(it)
+ }
+
+ sectionsPagerAdapter.verseListFrag.arrayAdapter.notifyDataSetChanged()
+ sectionsPagerAdapter.getItem(bookTabPosition)
+ sectionsPagerAdapter.getItem(wordTabPosition)
+ sectionsPagerAdapter.notifyDataSetChanged()
+
+ val tabHost = this.findViewById(R.id.tabs) as TabLayout
+ tabHost.getTabAt(verseTabPosition)!!.text = CommonUtils.resources.getString(R.string.verse_count, mSearchResultsArray.count().toString()) // For some reason the value set in 'setResultsAdapter' get's cleared so I need to do it here as well.
+ tabHost.getTabAt(bookTabPosition)!!.text = CommonUtils.resources.getString(R.string.book_count, bookStatistics.count().toString())
+ tabHost.getTabAt(wordTabPosition)!!.text = CommonUtils.resources.getString(R.string.word_count, wordStatistics.count().toString())
+
+ }
+ /**
+ * Handle scripture/Appendix toggle
+ */
+ private val scriptureToggleClickListener = View.OnClickListener {
+ isScriptureResultsCurrentlyShown = !isScriptureResultsCurrentlyShown
+ populateViewResultsAdapter()
+// mKeyArrayAdapter!!.notifyDataSetChanged()
+ searchResultsActionBarManager.setScriptureShown(isScriptureResultsCurrentlyShown)
+ }
+
+ companion object {
+ private const val TAG = " MySearchResults"
+ private const val LIST_ITEM_TYPE = android.R.layout.simple_list_item_2
+ }
+}
+
+class SearchResultsPagerAdapter(private val context: Context, fm: FragmentManager,
+ searchControl: SearchControl,
+ windowControl: WindowControl,
+ intent: Intent,
+ private val mSearchResultsArray:ArrayList,
+ private val bookStatistics: MutableList,
+ private val wordStatistics: MutableList,
+ private val keyWordStatistics: MutableList
+) :
+ FragmentPagerAdapter(fm) {
+ val searchControl = searchControl
+ val activeWindowPageManagerProvider = windowControl.activeWindowPageManager
+ val intent = intent
+ val windowControl = windowControl
+ lateinit var verseListFrag: SearchResultsFragment
+
+
+ override fun getItem(position: Int): Fragment {
+ // getItem is called to instantiate the fragment for the given page.
+ // This initialises the fragment but only if POSITION_NONE is returned from getItemPosition.
+ // This happens naturally when pages are moving in and out of view but not when we try to refresh.
+ // The individual fragment code gets called twice - first with no data to display and then with the correct data.
+ // This is because the page is shown in the GlobalScope in order to allow us to show a progress indicator.
+ val frag: Fragment
+ when (position) {
+ verseTabPosition -> {
+ frag = SearchResultsFragment(mSearchResultsArray)
+// val bundle = Bundle()
+// bundle.putString("edttext", "From Activity")
+// bundle.putParcelableArrayList("VerseResultList", mSearchResultsArray)
+// frag.setArguments(bundle)
+ frag.searchControl = searchControl
+ frag.windowControl = windowControl
+// frag.activeWindowPageManagerProvider = activeWindowPageManagerProvider
+ frag.intent = intent
+ verseListFrag = frag
+ }
+ bookTabPosition-> {
+ frag = SearchBookStatisticsFragment()
+ frag.bookStatistics = bookStatistics
+ frag.searchResultsArray = mSearchResultsArray
+// bookStatisticsFrag = frag
+ }
+ 2-> {
+ frag = SearchWordStatisticsFragment()
+ frag.searchResultsArray = mSearchResultsArray
+ frag.wordStatistics = wordStatistics
+ frag.keyWordStatistics = keyWordStatistics
+ }
+ else -> frag = PlaceholderFragment.newInstance(1)
+ }
+ return frag
+ }
+ override fun getItemPosition(`object`: Any): Int {
+ // This line is needed to force a refresh of the fragment.
+ // It doesn't seem to affect anything adversely, I think because all tabs remain in memory the whole time.
+ return POSITION_NONE
+ }
+ override fun getCount(): Int {
+ return 3
+ }
+}
diff --git a/app/src/main/java/net/bible/android/view/activity/search/Search.kt b/app/src/main/java/net/bible/android/view/activity/search/Search.kt
index 717bb87613..80494d3d6b 100644
--- a/app/src/main/java/net/bible/android/view/activity/search/Search.kt
+++ b/app/src/main/java/net/bible/android/view/activity/search/Search.kt
@@ -20,12 +20,16 @@ package net.bible.android.view.activity.search
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
+import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
+import android.widget.SeekBar
import net.bible.android.activity.R
import net.bible.android.activity.databinding.SearchBinding
@@ -63,6 +67,31 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
/** get all, any, phrase query limitation
*/
+
+ private var fuzzySearchAccuracySelection: Int = 5
+ set(value) {
+ binding.fuzzySearch.text = getString(R.string.search_fuzzy, "(~" + (value * 10).toString() + "%)")
+ binding.fuzzySearchAccuracy.setProgress(value)
+ binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString())
+ field = value
+ }
+
+ // I don't know how to do this properly. I want to update the value of proximityWordNumber whenever it is needed (buttons, text, initialising)
+ // But part of the update is to set the text value itself. But this gets into an infinite loop of course when called when the text value is updated.
+ // So i have broken it into two parts. One updates everything and one does not update the text field.
+ private var proximitySearchWordsSelection: Int = 10
+ set(value) {
+ binding.proximityWordNumber.setText(value.toString())
+ proximitySearchWordsSelectionFinal = value
+ binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString())
+ field = value
+ }
+ private var proximitySearchWordsSelectionFinal: Int = 10
+ set(value) {
+ binding.proximitySearch.text = getString(R.string.search_proximity, "(${value.toString()} ${getString(R.string.words)})")
+ field = value
+ }
+
private val searchType: SearchType
get() {
return when (wordsRadioSelection) {
@@ -75,7 +104,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
}
}
}
-
/** get OT, NT, or all query limitation
*
* @return
@@ -104,6 +132,12 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
CommonUtils.settings.setLong("search-last-used", System.currentTimeMillis())
buildActivityComponent().inject(this)
+ // set text for current bible book on appropriate radio button
+ val currentBookRadioButton = findViewById(R.id.searchCurrentBook) as RadioButton
+
+ // set current book to default and allow override if saved - implies returning via Back button
+ currentBookName = searchControl.currentBookName
+
if (!searchControl.validateIndex(documentToSearch)) {
Dialogs.showErrorMsg(R.string.error_occurred) { finish() }
}
@@ -117,6 +151,88 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
}
else -> false
}}
+ binding.searchText.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {}
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString())
+ }
+ })
+ val sectionRadioGroup = findViewById(R.id.bibleSectionGroup) as RadioGroup
+ sectionRadioGroup.setOnCheckedChangeListener { group, checkedId ->
+ sectionRadioSelection = checkedId
+ CommonUtils.settings.setInt("search_bible_section_group_prompt", checkedId)
+ enableSearchControls()
+ }
+ val wordsRadioGroup = findViewById(R.id.wordsGroup) as RadioGroup
+ wordsRadioGroup.setOnCheckedChangeListener { group, checkedId ->
+ wordsRadioSelection = checkedId
+ CommonUtils.settings.setInt("search_words_group_prompt", checkedId)
+ enableSearchControls()
+ }
+ binding.includeAllEndings.setOnClickListener {
+ CommonUtils.settings.setBoolean("search_include_all_endings", binding.includeAllEndings.isChecked)
+ if (binding.includeAllEndings.isChecked) {
+ binding.proximitySearch.isChecked = false
+ binding.strongsSearch.isChecked = false
+ binding.fuzzySearch.isChecked = false
+ }
+ enableSearchControls()
+ }
+ binding.rememberSearchText.setOnClickListener {
+ CommonUtils.settings.setBoolean("search_remember_search_text", binding.rememberSearchText.isChecked)
+ }
+ binding.fuzzySearch.setOnClickListener {
+ if (binding.fuzzySearch.isChecked) {
+ binding.proximitySearch.isChecked = false
+ binding.strongsSearch.isChecked = false
+ binding.includeAllEndings.isChecked = false
+ }
+ enableSearchControls()
+ }
+ binding.fuzzySearchAccuracy.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+ fuzzySearchAccuracySelection = progress
+ }
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {}
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {}
+ })
+
+ binding.proximitySearch.setOnClickListener {
+ if (binding.proximitySearch.isChecked) {
+ binding.fuzzySearch.isChecked = false
+ binding.strongsSearch.isChecked = false
+ binding.includeAllEndings.isChecked = false
+ }
+ enableSearchControls()
+ }
+ binding.proximityButtonAdd.setOnClickListener {proximitySearchWordsSelection = binding.proximityWordNumber.text.toString().toInt() + 1}
+ binding.proximityButtonSubtract.setOnClickListener {proximitySearchWordsSelection = binding.proximityWordNumber.text.toString().toInt() - 1}
+ binding.proximityWordNumber.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {}
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {proximitySearchWordsSelectionFinal = s.toString().toInt()}
+ })
+
+ binding.strongsSearch.setOnClickListener {
+ if (binding.strongsSearch.isChecked) {
+ binding.includeAllEndings.isChecked = false
+ binding.fuzzySearch.isChecked = false
+ binding.proximitySearch.isChecked = false
+ }
+ enableSearchControls()
+ }
+ binding.greekOrHebrewGroup.setOnCheckedChangeListener { group, checkedId ->
+ CommonUtils.settings.setInt("search_greek_or_hebrew", checkedId)
+ enableSearchControls()
+ }
+ binding.decoratedTextShow.setOnCheckedChangeListener { group, checkedId ->
+ // I would like to show the actual text that is being used for the search somewhere.
+ // I think it would be good to show it on the list screen. Could also show it here.
+
+ CommonUtils.settings.setBoolean("search_show_decorated_text", checkedId)
+ enableSearchControls()
+ }
binding.submit.setOnClickListener { onSearch() }
//searchText.setOnKeyListener(OnKeyListener { v, keyCode, event ->
@@ -129,6 +245,27 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
// false
//})
+ // Initialise controls
+ binding.includeAllEndings.isChecked = CommonUtils.settings.getBoolean("search_include_all_endings", false)
+ binding.rememberSearchText.isChecked = CommonUtils.settings.getBoolean("search_remember_search_text", false)
+
+ binding.proximitySearch.isChecked = CommonUtils.settings.getBoolean("search_proximity",false)
+ proximitySearchWordsSelection = CommonUtils.settings.getInt("search_proximity_words", 10)
+
+ binding.wordsGroup.check(CommonUtils.settings.getInt("search_words_group_prompt", 0))
+ wordsRadioSelection = binding.wordsGroup.checkedRadioButtonId
+
+ binding.bibleSectionGroup.check(CommonUtils.settings.getInt("search_bible_section_group_prompt", 0))
+ sectionRadioSelection = binding.bibleSectionGroup.checkedRadioButtonId
+
+ binding.fuzzySearch.isChecked = CommonUtils.settings.getBoolean("search_fuzzy", false)
+ fuzzySearchAccuracySelection = CommonUtils.settings.getInt("search_fuzzy_accuracy", 5)
+
+ binding.strongsSearch.isChecked = CommonUtils.settings.getBoolean("search_strongs",false)
+ binding.greekOrHebrewGroup.check(CommonUtils.settings.getInt("search_greek_or_hebrew",0))
+
+ binding.decoratedTextShow.isChecked = CommonUtils.settings.getBoolean("search_show_decorated_text",false)
+
// pre-load search string if passed in
val extras = intent.extras
if (extras != null) {
@@ -136,10 +273,10 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
if (StringUtils.isNotEmpty(text)) {
binding.searchText.setText(text)
}
+ } else {
+ if (binding.rememberSearchText.isChecked) binding.searchText.setText(CommonUtils.settings.getString("search_text", ""))
}
- val wordsRadioGroup = findViewById(R.id.wordsGroup) as RadioGroup
- wordsRadioGroup.setOnCheckedChangeListener { group, checkedId -> wordsRadioSelection = checkedId }
if (extras != null) {
val wordsSelection = extras.getInt(WORDS_SELECTION_SAVE, -1)
if (wordsSelection != -1) {
@@ -147,8 +284,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
}
}
- val sectionRadioGroup = findViewById(R.id.bibleSectionGroup) as RadioGroup
- sectionRadioGroup.setOnCheckedChangeListener { group, checkedId -> sectionRadioSelection = checkedId }
if (extras != null) {
val sectionSelection = extras.getInt(SECTION_SELECTION_SAVE, -1)
if (sectionSelection != -1) {
@@ -156,11 +291,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
}
}
- // set text for current bible book on appropriate radio button
- val currentBookRadioButton = findViewById(R.id.searchCurrentBook) as RadioButton
-
- // set current book to default and allow override if saved - implies returning via Back button
- currentBookName = searchControl.currentBookName
if (extras != null) {
val currentBibleBookSaved = extras.getString(CURRENT_BIBLE_BOOK_SAVE)
if (currentBibleBookSaved != null) {
@@ -169,9 +299,40 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
}
currentBookRadioButton.text = currentBookName
+ binding.textClear.setOnClickListener({binding.searchText.setText("")})
+ enableSearchControls()
+
Log.i(TAG, "Finished displaying Search view")
}
+ fun enableSearchControls() {
+
+ // Set control visibility
+ binding.fuzzySearchDetailsLayout.visibility = if (binding.fuzzySearch.isChecked) View.VISIBLE else View.GONE
+ binding.proximityDetailsLayout.visibility = if (binding.proximitySearch.isChecked) View.VISIBLE else View.GONE
+ binding.strongsDetailLayout.visibility = if (binding.strongsSearch.isChecked) View.VISIBLE else View.GONE
+ binding.decoratedSearchDetailLayout.visibility = if (binding.decoratedTextShow.isChecked) View.VISIBLE else View.GONE
+
+ enableLayout(binding.fuzzySearchLayout, binding.allWords.id == wordsRadioSelection)
+ enableLayout(binding.fuzzySearchDetailsLayout, binding.fuzzySearch.isChecked && binding.fuzzySearch.isEnabled)
+
+ enableLayout(binding.strongsDetailLayout, (binding.strongsSearch.isChecked && binding.strongsSearch.isEnabled))
+
+ binding.searchOldTestament.isEnabled = !binding.strongsSearch.isChecked || (binding.strongsSearch.isEnabled && binding.strongsSearch.isChecked && binding.searchHebrew.isChecked)
+ binding.searchNewTestament.isEnabled = !binding.strongsSearch.isChecked || (binding.strongsSearch.isEnabled && binding.strongsSearch.isChecked && binding.searchGreek.isChecked)
+
+ binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString())
+ }
+
+ fun enableLayout(layout: LinearLayout, isEnabled: Boolean) {
+ for (i in 0 until layout.childCount) {
+ val view: View = layout.getChildAt(i)
+ if (view is LinearLayout) {enableLayout(view,isEnabled)}
+ view.isEnabled = isEnabled
+ }
+ layout.isEnabled = isEnabled
+ }
+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.rebuildIndex -> {
@@ -191,7 +352,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
binding.searchText.requestFocus()
}
-
fun onRebuildIndex(v: View?) {
startActivity(Intent(this, SearchIndex::class.java))
finish()
@@ -204,6 +364,13 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
var text = binding.searchText.text.toString()
if (!StringUtils.isEmpty(text)) {
+ CommonUtils.settings.setString("search_text", text)
+ CommonUtils.settings.setBoolean("search_fuzzy", binding.fuzzySearch.isChecked)
+ CommonUtils.settings.setInt("search_fuzzy_accuracy", fuzzySearchAccuracySelection)
+ CommonUtils.settings.setBoolean("search_proximity", binding.proximitySearch.isChecked)
+ CommonUtils.settings.setInt("search_proximity_words", proximitySearchWordsSelectionFinal)
+ CommonUtils.settings.setBoolean("search_strongs", binding.strongsSearch.isChecked)
+
// update current intent so search is restored if we return here via history/back
// the current intent is saved by HistoryManager
intent.putExtra(SEARCH_TEXT_SAVE, text)
@@ -211,12 +378,13 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
intent.putExtra(SECTION_SELECTION_SAVE, sectionRadioSelection)
intent.putExtra(CURRENT_BIBLE_BOOK_SAVE, currentBookName)
- text = searchControl.decorateSearchString(text, searchType, bibleSection, currentBookName)
+// text = searchControl.decorateSearchString(text, searchType, bibleSection, currentBookName)
+ text = decorateSearchString(text)
Log.i(TAG, "Search text:$text")
// specify search string and doc in new Intent;
// if doc is not specifed a, possibly invalid, doc may be used when returning to search via history list e.g. search bible, select dict, history list, search results
- val intent = Intent(this, SearchResults::class.java)
+ val intent = Intent(this, MySearchResults::class.java)
intent.putExtra(SearchControl.SEARCH_TEXT, text)
val currentDocInitials = documentToSearch.initials
intent.putExtra(SearchControl.SEARCH_DOCUMENT, currentDocInitials)
@@ -228,6 +396,14 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) {
}
}
+ private fun decorateSearchString(searchString: String): String {
+ val fuzzyAccuracy = if (binding.fuzzySearch.isChecked && binding.fuzzySearch.isEnabled) fuzzySearchAccuracySelection.toDouble()/10 else null
+ val proximityWords = if (binding.proximitySearch.isChecked && binding.proximitySearch.isEnabled) proximitySearchWordsSelectionFinal else null
+ val strongs = if (binding.strongsSearch.isChecked && binding.strongsSearch.isEnabled) {if (binding.searchGreek.isChecked) 'G' else 'H'} else null
+
+ return searchControl.decorateSearchString(searchString, searchType, bibleSection, currentBookName, binding.includeAllEndings.isChecked, fuzzyAccuracy, proximityWords, strongs)
+ }
+
companion object {
private const val SEARCH_TEXT_SAVE = "Search"
diff --git a/app/src/main/java/net/bible/android/view/activity/search/SearchHighlight.kt b/app/src/main/java/net/bible/android/view/activity/search/SearchHighlight.kt
new file mode 100644
index 0000000000..ac3707619e
--- /dev/null
+++ b/app/src/main/java/net/bible/android/view/activity/search/SearchHighlight.kt
@@ -0,0 +1,191 @@
+package net.bible.android.view.activity.search
+
+import android.graphics.Typeface
+import android.text.Html
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import android.util.Log
+import org.jdom2.Element
+import org.jdom2.Text
+import java.lang.Exception
+import java.util.regex.Pattern
+
+class SearchHighlight(searchTerms: String) {
+
+ val isStrongsSearch = searchTerms.contains("strong:")
+ val elementsToExclude: List = listOf("note", "reference")
+ val elementsToInclude: List = listOf("w", "transChange", "divineName", "seg")
+ val consecutiveWordsRegex = Regex("(]*>)([^<>]*?)(<\\/b>)(\\s+)\\1([\\s\\S]*?)\\3", RegexOption.IGNORE_CASE)
+ val strongsSearchPattern: Pattern = Pattern.compile(prepareStrongsSearchTerm(searchTerms) + "[a-z]?\\b", Pattern.CASE_INSENSITIVE) // search on a word boundary (eg find strong:g0123 not strong:g01234567
+
+ private val preparedSearchWordsPatternList:List = // Build a list of Patterns representing each search word.
+ splitSearchTerms(searchTerms).map {
+ var searchWord = prepareSearchWord(it)
+ searchWord = if (it.contains("*") or it.contains("~")) {
+ "\\b$searchWord[\\w\\'\\-]*\\b" // Match whole words including with hyphens and apostrophes
+ } else {
+ "\\b$searchWord\\b"
+ }
+ Pattern.compile(searchWord, Pattern.CASE_INSENSITIVE)
+ }
+
+ fun generateSpannableFromVerseElement(verseElement: Element): SpannableString {
+ /* Takes a verse in Element form and rebuilds the verse in string form.
+ * Interestingly it is faster to get the text in Element form and convert it myself than to call 'getSearchResultVerseText'.
+ * Once the verse is in String format which includes tags for Strongs numbers it is passed to 'generateSpannableFromVerseString'
+ * which does the actual conversion to a spannable.
+ `*/
+ var verseString = ""
+ try {
+ // TODO: Strongs searches are handled differently to normal searches and so cannot be combined either with a normal search term or other strongs searches. This should be done better.
+
+ // Part 1: Highlight any strongs words. The raw verse text is returned with added for the strongs words. We always need to process this just to get the plaintext
+ val verses = verseElement.getChildren("verse")
+
+ for (verse in verses) {
+ verseString += processElementChildren(verse, "", false)
+ }
+
+ if (isStrongsSearch) {
+ // Check for highlighted consecutive words and merge them into a single highlighted phrase. Some translations will indicate multiple
+ // consecutive lemma spans with the same strongs number when in fact all spans represent the single original language word.
+ // This messes with the search statistics as it uses the bolded words to tally search hits by word.
+ var verseStringLength = 0
+ while (verseStringLength != verseString.length) {
+ verseStringLength = verseString.length
+ // This pattern matches two consecutive tag spans separate only by white space and replaces them with a single tag.
+ val match = consecutiveWordsRegex.find(verseString)
+ if (match != null) {
+ val matches = match.groupValues
+ verseString = verseString.replace(
+ matches[0],
+ "${matches[1]}${matches[2]}${matches[4]}${matches[5]}${matches[3]}"
+ )
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.w("SEARCH", e.message!!)
+ } finally {
+ return generateSpannableFromVerseString(verseString)
+ }
+ }
+
+ fun generateSpannableFromVerseString(verseString:String): SpannableString {
+
+ val spannableText = SpannableString(Html.fromHtml(verseString)) // We started with an XML verse which got turned into a string with tags which is now turned into a spannable
+ try {
+
+ // Part 2: Find and highlight the normal (non-strongs) words or phrases in the PLAIN text verse
+ for (searchPattern in preparedSearchWordsPatternList) {
+ val m = searchPattern.matcher(spannableText)
+ while (m.find()) {
+ spannableText.setSpan(
+ StyleSpan(Typeface.BOLD),
+ m.start(),
+ m.end(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ }
+
+ } catch (e: Exception) {
+ Log.w("SEARCH", e.message!!)
+ } finally {
+ return spannableText
+ }
+ }
+
+ private fun processElementChildren(
+ parentElement: Element,
+ verseStringInitial: String?,
+ isBoldInitial: Boolean
+ ): String? {
+ // Recursive method to walk the verse element tree ignoring tags like 'note' that should not be shown in the search results
+ // and including tags like 'w' that should be included. This routine is needed only to do searches on lemma attributes. That
+ // is why bolding only occurs in that part of the code.
+ var verseString = verseStringInitial
+ var isBold = isBoldInitial
+ for (el in parentElement.content) {
+ if (el is Element) {
+ if (elementsToInclude.contains(el.name)) {
+ isBold = try {
+ if (isStrongsSearch) {
+ val lemma = el.getAttributeValue("lemma")
+ lemma != null && strongsSearchPattern.matcher(lemma.trim { it <= ' ' }).find()
+ } else {
+ false
+ }
+ } catch (e: Exception) {
+ false
+ }
+ // Only leaf nodes should have their text appended. If a node has child tags, the text will be passed as one of the children .
+ if (el.children.isEmpty()) verseString += buildElementText(el.text, isBold)
+ }
+ if (el.children.isNotEmpty() && !elementsToExclude.contains(el.name)) {
+ verseString = processElementChildren(el, verseString, isBold)
+ }
+ } else if (el is Text) {
+ verseString += buildElementText(el.text, false)
+ } else {
+ verseString += buildElementText(el.toString(), false)
+ }
+ }
+ return verseString
+ }
+
+ private fun buildElementText(elementText: String, isBold: Boolean): String {
+ return if (isBold) {
+ String.format("%s", elementText)
+ } else {
+ elementText
+ }
+ }
+
+ private fun prepareStrongsSearchTerm(_searchTerms: String): String {
+ // Replaces strong:g00123 or strong:g123 with REGEX strong:g0*123. This is needed because the search term submitted by the 'Find all occurrences includes extra zeros
+ // The capitalisation is not important since we do a case insensitive search
+ var searchTerms = _searchTerms
+ searchTerms = searchTerms.replace("strong:g0*".toRegex(RegexOption.IGNORE_CASE), "strong:g0*")
+ searchTerms = searchTerms.replace("strong:h0*".toRegex(RegexOption.IGNORE_CASE), "strong:h0*")
+ searchTerms = searchTerms.replace("+", "") // Remove + which indicates AND searches
+
+ return searchTerms
+ }
+
+ private fun splitSearchTerms(searchTerms: String): Array {
+ // Find the individual words that we are looking for so we can highlight them.
+ // Proximity searches wrap the words in quotes the quotes need to be removed.
+ val fuzzySearch = searchTerms.indexOf("~")
+ val newSearchTerms = if (fuzzySearch > -1) {
+ searchTerms.substring(0,fuzzySearch).replace("\"","")
+ } else searchTerms
+ // Split the search terms on space characters that are not enclosed in double quotes.
+ // Eg: 'moses "burning bush"' -> "moses" and "burning bush"
+ return newSearchTerms.split("\\s+(?=(?:\"(?:\\\\\"|[^\"])+\"|[^\"])+$)".toRegex()).toTypedArray()
+ }
+
+ private fun prepareSearchWord(searchWord: String): String {
+ // Need to clean up the search word itself before trying to find the searchWord in the text. Routine is called for each part of the search term
+ // Eg: '+"burning bush"' -> 'burning bush'
+ val fuzzySearch = searchWord.indexOf("~")
+ var newSearchWord = if (fuzzySearch > -1) { searchWord.substring(0,fuzzySearch) } else searchWord
+ newSearchWord = newSearchWord.replace("\"", "") // Remove quotes which indicate phrase searches
+ newSearchWord = newSearchWord.replace("+", "") // Remove + which indicates AND searches
+ newSearchWord = newSearchWord.replace("?", "\\p{L}") // Handles any letter from any language
+ if (newSearchWord.isNotEmpty()) {
+ newSearchWord = if ((newSearchWord.substring(newSearchWord.length - 1) == "*") or (fuzzySearch>-1)){
+ // The last character in the search is a * so remove it since the default sword search assumes a wildcard search
+ newSearchWord.replace("*", "")
+ } else {
+ // A * found inside a search term should probably be ignored.
+ // It can happen normally as part of a strongs search since the regex expression uses a * to find any number of leading 0s.
+ newSearchWord.replace("*", "\b") // Match on a word boundary - I am not sure why this was needed.
+
+ }
+ }
+ return newSearchWord
+ }
+
+}
diff --git a/app/src/main/java/net/bible/android/view/activity/search/SearchResultsAdapter.java b/app/src/main/java/net/bible/android/view/activity/search/SearchResultsAdapter.java
new file mode 100644
index 0000000000..9bec39ee3f
--- /dev/null
+++ b/app/src/main/java/net/bible/android/view/activity/search/SearchResultsAdapter.java
@@ -0,0 +1,105 @@
+package net.bible.android.view.activity.search;
+
+import android.content.Context;
+import android.text.Html;
+import android.text.SpannableString;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import net.bible.android.activity.R;
+
+import java.util.ArrayList;
+
+import org.crosswire.jsword.book.Book;
+import org.crosswire.jsword.book.Books;
+import org.crosswire.jsword.passage.Key;
+import org.crosswire.jsword.passage.NoSuchKeyException;
+
+public class SearchResultsAdapter extends ArrayAdapter {
+
+ private int resource;
+ private ArrayList arrayList;
+ private Context context;
+
+ public SearchResultsAdapter(Context _context, int _resource, ArrayList arrayList) {
+ super(_context, _resource, arrayList);
+ this.resource = _resource;
+ this.arrayList=arrayList;
+ this.context=_context;
+ }
+
+ private void scaleTextView(TextView textView, Float scale) {
+ if (textView.getTag()==null) textView.setTag(Float.valueOf(textView.getTextSize()));
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (Float) textView.getTag() * scale);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SearchResultsData resultData=arrayList.get(position);
+ Float scaleText = 1.2F; // TODO: I would like to add an application preference that allows the user to set the size of their results list
+ if(convertView==null) {
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView=inflater.inflate(R.layout.search_results_statistics_row_verse, null);
+
+ TextView reference=convertView.findViewById(R.id.reference);
+ reference.setText(resultData.reference);
+ scaleTextView(reference, scaleText);
+
+ TextView translation=convertView.findViewById(R.id.translation);
+ translation.setText(resultData.translation);
+ scaleTextView(translation, scaleText);
+
+ // Get the text of the verse
+ Book book = Books.installed().getBook(resultData.translation);
+ try {
+ Key key = book.getKey(resultData.osisKey);
+
+ String verseHtml = resultData.verseHtml.trim();
+
+ // The 'toHtml' method wraps the string in
which puts a large margin at the bottom of the verse. Need to remove this.
+ try{
+ verseHtml= replaceString(verseHtml,"
", "");
+ verseHtml= replaceString(verseHtml,"
", "");
+ }catch (Exception e) {}
+
+ SpannableString verseTextHtml = new SpannableString(Html.fromHtml(verseHtml));
+
+ TextView verse=convertView.findViewById(R.id.verse);
+ verse.setText(verseTextHtml);
+ scaleTextView(verse, scaleText);
+
+ } catch (NoSuchKeyException e) {
+ e.printStackTrace();
+ }
+ }
+ return convertView;
+ }
+
+ private String replaceString(String initialString, String first, String second)
+ {
+ StringBuilder b = new StringBuilder(initialString);
+ b.replace(initialString.lastIndexOf(first), initialString.lastIndexOf(first)+first.length(),second );
+ return b.toString();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position;
+ }
+ @Override
+ public int getViewTypeCount() {
+ if(getCount()<1) return 1;
+ return getCount();
+ }
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+
+}
diff --git a/app/src/main/java/net/bible/android/view/activity/search/SearchResultsFragments.kt b/app/src/main/java/net/bible/android/view/activity/search/SearchResultsFragments.kt
new file mode 100644
index 0000000000..6dd5e8ce9e
--- /dev/null
+++ b/app/src/main/java/net/bible/android/view/activity/search/SearchResultsFragments.kt
@@ -0,0 +1,295 @@
+package net.bible.android.view.activity.search
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Button
+import android.widget.FrameLayout
+import androidx.fragment.app.Fragment
+import android.widget.ListView
+import android.widget.ProgressBar
+import android.widget.TextView
+import android.widget.Toast
+import com.google.android.material.tabs.TabLayout
+import net.bible.android.activity.databinding.SearchResultsStatisticsFragmentVerseBinding
+import net.bible.android.activity.R
+import net.bible.android.activity.databinding.SearchResultsStatisticsFragmentByBinding
+import net.bible.android.control.search.SearchControl
+import net.bible.android.view.activity.base.Dialogs
+import net.bible.android.view.activity.page.MainBibleActivity
+import org.apache.commons.lang3.StringUtils
+import org.crosswire.jsword.passage.Key
+import net.bible.service.common.CommonUtils
+import net.bible.service.sword.SwordDocumentFacade
+import net.bible.android.control.page.window.WindowControl
+import javax.inject.Inject
+
+private lateinit var displayedResultsArray: ArrayList
+private var isSearchResultsFiltered = false
+
+
+class PlaceholderFragment: Fragment() {
+
+ private var _binding: SearchResultsStatisticsFragmentVerseBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+
+ _binding = SearchResultsStatisticsFragmentVerseBinding.inflate(inflater, container, false)
+ val root = binding.root
+
+ return root
+ }
+
+ companion object {
+ /**
+ * The fragment argument representing the section number for this
+ * fragment.
+ */
+ private const val ARG_SECTION_NUMBER = "section_number"
+
+ /**
+ * Returns a new instance of this fragment for the given section
+ * number.
+ */
+ @JvmStatic
+ fun newInstance(sectionNumber: Int): PlaceholderFragment {
+ return PlaceholderFragment().apply {
+ arguments = Bundle().apply {
+ putInt(ARG_SECTION_NUMBER, sectionNumber)
+ }
+ }
+ }
+ }
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
+
+private fun setResultsAdapter(resultsArray:ArrayList, activity: Activity): SearchResultsAdapter {
+ // This function is called from both the SearchResultsFragment and the SearchWordStatistics (for filtering)
+ val tabhost = activity.findViewById(R.id.tabs) as TabLayout
+ val verseCount = if(resultsArray.count()==0) "..." else resultsArray.count().toString()
+ tabhost.getTabAt(verseTabPosition)!!.text = CommonUtils.resources.getString(R.string.verse_count, verseCount) // The count needs to be set here because it can be changed when filtering the list by word
+
+ displayedResultsArray = resultsArray
+ return SearchResultsAdapter(activity, android.R.layout.simple_list_item_2,
+ displayedResultsArray as java.util.ArrayList?
+ )
+}
+class SearchResultsFragment(val mSearchResultsArray:ArrayList) : Fragment() {
+ private var _binding: SearchResultsStatisticsFragmentVerseBinding? = null
+ private val binding get() = _binding!!
+ lateinit var arrayAdapter: ArrayAdapter
+
+ var searchControl: SearchControl? = null
+ lateinit var intent: Intent
+ lateinit var windowControl: WindowControl
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+
+ _binding = SearchResultsStatisticsFragmentVerseBinding.inflate(inflater, container, false)
+ val root = binding.root
+
+ val resultList: ListView = binding.searchResultsList
+
+ arrayAdapter = setResultsAdapter(mSearchResultsArray, requireActivity())
+ resultList.adapter = arrayAdapter
+ resultList.descendantFocusability = ViewGroup.FOCUS_BEFORE_DESCENDANTS;
+ resultList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ resultList.isVerticalScrollBarEnabled = false
+
+ resultList.setOnItemClickListener { parent, view, position, id ->
+ Toast.makeText(resultList.context, "Hi",Toast.LENGTH_LONG)
+ try { // no need to call HistoryManager.addHistoryItem() here because PassageChangeMediator will tell HistoryManager a change is about to occur
+ verseSelected(mCurrentlyDisplayedSearchResults[displayedResultsArray[position].id!!])
+ } catch (e: Exception) {
+ Log.e("SearchResults", "Selection error", e)
+ Dialogs.showErrorMsg(R.string.error_occurred, e)
+ }
+ }
+ return root
+ }
+
+ private fun verseSelected(key: Key?) {
+ Log.i("SearchResults.TAG", "chose:$key")
+ if (key != null) { // which doc do we show
+ searchControl
+ var targetDocInitials = intent.extras!!.getString(SearchControl.TARGET_DOCUMENT)
+ if (StringUtils.isEmpty(targetDocInitials)) {
+ targetDocInitials = windowControl.activeWindowPageManager.currentPage.currentDocument!!.initials
+ }
+ val swordDocumentFacade = SwordDocumentFacade()
+ val targetBook = swordDocumentFacade.getDocumentByInitials(targetDocInitials)
+ windowControl.activeWindowPageManager.setCurrentDocumentAndKey(targetBook, key)
+ // this also calls finish() on this Activity. If a user re-selects from HistoryList then a new Activity is created
+ val intent = Intent(this.context, MainBibleActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+ }
+ startActivity(intent)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
+
+class SearchBookStatisticsFragment : Fragment() {
+ private var _binding: SearchResultsStatisticsFragmentByBinding? = null
+
+ private val binding get() = _binding!!
+ var bookStatistics = mutableListOf()
+ lateinit var searchResultsArray: ArrayList
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ // Inflate the layout for this fragment
+ _binding = SearchResultsStatisticsFragmentByBinding.inflate(inflater, container, false)
+ val root = binding.root
+
+ val statisticsLayout = binding.statisticsLayout
+ binding.showKeyWordOnly.visibility = View.GONE
+
+ val maxCount: Int = bookStatistics.maxOfOrNull { it.count } ?: 0
+ bookStatistics.map {
+
+ val statsRow: View = inflater.inflate(
+ R.layout.search_results_statistics_row_by_book,
+ statisticsLayout, false
+ )
+ var button = statsRow.findViewById