search term parsing + highlighting
This commit is contained in:
		
							parent
							
								
									542a06f08a
								
							
						
					
					
						commit
						27f31446c0
					
				| @ -50,6 +50,7 @@ import eu.siacs.conversations.entities.Roster; | |||||||
| import eu.siacs.conversations.entities.ServiceDiscoveryResult; | import eu.siacs.conversations.entities.ServiceDiscoveryResult; | ||||||
| import eu.siacs.conversations.services.ShortcutService; | import eu.siacs.conversations.services.ShortcutService; | ||||||
| import eu.siacs.conversations.utils.CryptoHelper; | import eu.siacs.conversations.utils.CryptoHelper; | ||||||
|  | import eu.siacs.conversations.utils.FtsUtils; | ||||||
| import eu.siacs.conversations.utils.MimeUtils; | import eu.siacs.conversations.utils.MimeUtils; | ||||||
| import eu.siacs.conversations.utils.Resolver; | import eu.siacs.conversations.utils.Resolver; | ||||||
| import eu.siacs.conversations.xmpp.mam.MamReference; | import eu.siacs.conversations.xmpp.mam.MamReference; | ||||||
| @ -229,6 +230,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { | |||||||
| 		db.execSQL(CREATE_IDENTITIES_STATEMENT); | 		db.execSQL(CREATE_IDENTITIES_STATEMENT); | ||||||
| 		db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); | 		db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); | ||||||
| 		db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); | 		db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); | ||||||
|  | 		db.execSQL(CREATE_MESSAGE_INDEX_TABLE); | ||||||
|  | 		db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); | ||||||
|  | 		db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); | ||||||
|  | 		db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| @ -718,10 +723,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { | |||||||
| 		return list; | 		return list; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public Cursor getMessageSearchCursor(String term) { | 	public Cursor getMessageSearchCursor(List<String> term) { | ||||||
| 		SQLiteDatabase db = this.getReadableDatabase(); | 		SQLiteDatabase db = this.getReadableDatabase(); | ||||||
| 		String SQL = "SELECT "+Message.TABLENAME+".*,"+Conversation.TABLENAME+'.'+Conversation.CONTACTJID+','+Conversation.TABLENAME+'.'+Conversation.ACCOUNT+','+Conversation.TABLENAME+'.'+Conversation.MODE+" FROM "+Message.TABLENAME +" join "+Conversation.TABLENAME+" on "+Message.TABLENAME+'.'+Message.CONVERSATION+'='+Conversation.TABLENAME+'.'+Conversation.UUID+" join messages_index ON messages_index.uuid=messages.uuid where "+Message.ENCRYPTION+" NOT IN("+Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE+','+Message.ENCRYPTION_PGP+','+Message.ENCRYPTION_DECRYPTION_FAILED+") AND messages_index.body MATCH ? ORDER BY "+Message.TIME_SENT+" DESC limit "+Config.MAX_SEARCH_RESULTS; | 		String SQL = "SELECT "+Message.TABLENAME+".*,"+Conversation.TABLENAME+'.'+Conversation.CONTACTJID+','+Conversation.TABLENAME+'.'+Conversation.ACCOUNT+','+Conversation.TABLENAME+'.'+Conversation.MODE+" FROM "+Message.TABLENAME +" join "+Conversation.TABLENAME+" on "+Message.TABLENAME+'.'+Message.CONVERSATION+'='+Conversation.TABLENAME+'.'+Conversation.UUID+" join messages_index ON messages_index.uuid=messages.uuid where "+Message.ENCRYPTION+" NOT IN("+Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE+','+Message.ENCRYPTION_PGP+','+Message.ENCRYPTION_DECRYPTION_FAILED+") AND messages_index.body MATCH ? ORDER BY "+Message.TIME_SENT+" DESC limit "+Config.MAX_SEARCH_RESULTS; | ||||||
| 		return db.rawQuery(SQL,new String[]{'%'+term+'%'}); | 		Log.d(Config.LOGTAG,"search term: "+FtsUtils.toMatchString(term)); | ||||||
|  | 		return db.rawQuery(SQL,new String[]{FtsUtils.toMatchString(term)}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public Iterable<Message> getMessagesIterable(final Conversation conversation) { | 	public Iterable<Message> getMessagesIterable(final Conversation conversation) { | ||||||
|  | |||||||
| @ -55,18 +55,18 @@ public class MessageSearchTask implements Runnable, Cancellable { | |||||||
| 	private static final ReplacingSerialSingleThreadExecutor EXECUTOR = new ReplacingSerialSingleThreadExecutor(MessageSearchTask.class.getName()); | 	private static final ReplacingSerialSingleThreadExecutor EXECUTOR = new ReplacingSerialSingleThreadExecutor(MessageSearchTask.class.getName()); | ||||||
| 
 | 
 | ||||||
| 	private final XmppConnectionService xmppConnectionService; | 	private final XmppConnectionService xmppConnectionService; | ||||||
| 	private final String term; | 	private final List<String> term; | ||||||
| 	private final OnSearchResultsAvailable onSearchResultsAvailable; | 	private final OnSearchResultsAvailable onSearchResultsAvailable; | ||||||
| 
 | 
 | ||||||
| 	private boolean isCancelled = false; | 	private boolean isCancelled = false; | ||||||
| 
 | 
 | ||||||
| 	private MessageSearchTask(XmppConnectionService xmppConnectionService, String term, OnSearchResultsAvailable onSearchResultsAvailable) { | 	private MessageSearchTask(XmppConnectionService xmppConnectionService, List<String> term, OnSearchResultsAvailable onSearchResultsAvailable) { | ||||||
| 		this.xmppConnectionService = xmppConnectionService; | 		this.xmppConnectionService = xmppConnectionService; | ||||||
| 		this.term = term; | 		this.term = term; | ||||||
| 		this.onSearchResultsAvailable = onSearchResultsAvailable; | 		this.onSearchResultsAvailable = onSearchResultsAvailable; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public static void search(XmppConnectionService xmppConnectionService, String term, OnSearchResultsAvailable onSearchResultsAvailable) { | 	public static void search(XmppConnectionService xmppConnectionService, List<String> term, OnSearchResultsAvailable onSearchResultsAvailable) { | ||||||
| 		new MessageSearchTask(xmppConnectionService, term, onSearchResultsAvailable).executeInBackground(); | 		new MessageSearchTask(xmppConnectionService, term, onSearchResultsAvailable).executeInBackground(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -535,7 +535,7 @@ public class XmppConnectionService extends Service { | |||||||
| 		return find(getConversations(), account, jid); | 		return find(getConversations(), account, jid); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public void search(String term, OnSearchResultsAvailable onSearchResultsAvailable) { | 	public void search(List<String> term, OnSearchResultsAvailable onSearchResultsAvailable) { | ||||||
| 		MessageSearchTask.search(this, term, onSearchResultsAvailable); | 		MessageSearchTask.search(this, term, onSearchResultsAvailable); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -64,6 +64,7 @@ import eu.siacs.conversations.ui.util.DateSeparator; | |||||||
| import eu.siacs.conversations.ui.util.Drawable; | import eu.siacs.conversations.ui.util.Drawable; | ||||||
| import eu.siacs.conversations.ui.util.ListViewUtils; | import eu.siacs.conversations.ui.util.ListViewUtils; | ||||||
| import eu.siacs.conversations.ui.util.ShareUtil; | import eu.siacs.conversations.ui.util.ShareUtil; | ||||||
|  | import eu.siacs.conversations.utils.FtsUtils; | ||||||
| import eu.siacs.conversations.utils.MessageUtils; | import eu.siacs.conversations.utils.MessageUtils; | ||||||
| 
 | 
 | ||||||
| import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; | import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; | ||||||
| @ -75,7 +76,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc | |||||||
| 	private MessageAdapter messageListAdapter; | 	private MessageAdapter messageListAdapter; | ||||||
| 	private final List<Message> messages = new ArrayList<>(); | 	private final List<Message> messages = new ArrayList<>(); | ||||||
| 	private WeakReference<Message> selectedMessageReference = new WeakReference<>(null); | 	private WeakReference<Message> selectedMessageReference = new WeakReference<>(null); | ||||||
| 	private final ChangeWatcher<String> currentSearch = new ChangeWatcher<>(); | 	private final ChangeWatcher<List<String>> currentSearch = new ChangeWatcher<>(); | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public void onCreate(final Bundle savedInstanceState) { | 	public void onCreate(final Bundle savedInstanceState) { | ||||||
| @ -153,13 +154,10 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private void quote(Message message) { | 	private void quote(Message message) { | ||||||
| 		String text = MessageUtils.prepareQuote(message); | 		switchToConversationAndQuote(wrap(message.getConversation()), MessageUtils.prepareQuote(message)); | ||||||
| 		final Conversational conversational = message.getConversation(); |  | ||||||
| 		switchToConversationAndQuote(wrap(message.getConversation()), text); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private Conversation wrap(Conversational conversational) { | 	private Conversation wrap(Conversational conversational) { | ||||||
| 		final Conversation conversation; |  | ||||||
| 		if (conversational instanceof Conversation) { | 		if (conversational instanceof Conversation) { | ||||||
| 			return (Conversation) conversational; | 			return (Conversation) conversational; | ||||||
| 		} else { | 		} else { | ||||||
| @ -205,12 +203,12 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc | |||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public void afterTextChanged(Editable s) { | 	public void afterTextChanged(Editable s) { | ||||||
| 		final String term = s.toString().trim(); | 		final List<String> term = FtsUtils.parse(s.toString().trim()); | ||||||
| 		if (!currentSearch.watch(term)) { | 		if (!currentSearch.watch(term)) { | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
| 		if (term.length() > 0) { | 		if (term.size() > 0) { | ||||||
| 			xmppConnectionService.search(s.toString().trim(), this); | 			xmppConnectionService.search(term, this); | ||||||
| 		} else { | 		} else { | ||||||
| 			MessageSearchTask.cancelRunningTasks(); | 			MessageSearchTask.cancelRunningTasks(); | ||||||
| 			this.messages.clear(); | 			this.messages.clear(); | ||||||
| @ -221,7 +219,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public void onSearchResultsAvailable(String term, List<Message> messages) { | 	public void onSearchResultsAvailable(List<String> term, List<Message> messages) { | ||||||
| 		runOnUiThread(() -> { | 		runOnUiThread(() -> { | ||||||
| 			this.messages.clear(); | 			this.messages.clear(); | ||||||
| 			messageListAdapter.setHighlightedTerm(term); | 			messageListAdapter.setHighlightedTerm(term); | ||||||
|  | |||||||
| @ -34,7 +34,6 @@ import android.view.ActionMode; | |||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
| import android.view.MenuItem; | import android.view.MenuItem; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.View.OnClickListener; |  | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.view.WindowManager; | import android.view.WindowManager; | ||||||
| import android.widget.ArrayAdapter; | import android.widget.ArrayAdapter; | ||||||
| @ -99,7 +98,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie | |||||||
| 					+ "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])" | 					+ "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])" | ||||||
| 					+ "|(?:\\%[a-fA-F0-9]{2}))+"); | 					+ "|(?:\\%[a-fA-F0-9]{2}))+"); | ||||||
| 
 | 
 | ||||||
| 	private String highlightedText = null; | 	private List<String> highlightedTerm = null; | ||||||
| 
 | 
 | ||||||
| 	private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> { | 	private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> { | ||||||
| 		if (url == null) { | 		if (url == null) { | ||||||
| @ -550,8 +549,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie | |||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); | 			StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); | ||||||
| 			if (highlightedText != null) { | 			if (highlightedTerm != null) { | ||||||
| 				StylingHelper.highlight(activity, body, highlightedText, StylingHelper.isDarkText(viewHolder.messageBody)); | 				StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			Linkify.addLinks(body, XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); | 			Linkify.addLinks(body, XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); | ||||||
| @ -1008,8 +1007,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public void setHighlightedTerm(String term) { | 	public void setHighlightedTerm(List<String> term) { | ||||||
| 		this.highlightedText = term; | 		this.highlightedTerm = term; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public interface OnQuoteListener { | 	public interface OnQuoteListener { | ||||||
|  | |||||||
| @ -35,6 +35,6 @@ import eu.siacs.conversations.entities.Message; | |||||||
| 
 | 
 | ||||||
| public interface OnSearchResultsAvailable { | public interface OnSearchResultsAvailable { | ||||||
| 
 | 
 | ||||||
| 	void onSearchResultsAvailable(String term, List<Message> messages); | 	void onSearchResultsAvailable(List<String> term, List<Message> messages); | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										94
									
								
								src/main/java/eu/siacs/conversations/utils/FtsUtils.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/main/java/eu/siacs/conversations/utils/FtsUtils.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright (c) 2018, Daniel Gultsch All rights reserved. | ||||||
|  |  * | ||||||
|  |  * Redistribution and use in source and binary forms, with or without modification, | ||||||
|  |  * are permitted provided that the following conditions are met: | ||||||
|  |  * | ||||||
|  |  * 1. Redistributions of source code must retain the above copyright notice, this | ||||||
|  |  * list of conditions and the following disclaimer. | ||||||
|  |  * | ||||||
|  |  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||||
|  |  * this list of conditions and the following disclaimer in the documentation and/or | ||||||
|  |  * other materials provided with the distribution. | ||||||
|  |  * | ||||||
|  |  * 3. Neither the name of the copyright holder nor the names of its contributors | ||||||
|  |  * may be used to endorse or promote products derived from this software without | ||||||
|  |  * specific prior written permission. | ||||||
|  |  * | ||||||
|  |  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||||||
|  |  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||||
|  |  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||||
|  |  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | ||||||
|  |  * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||||
|  |  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||||
|  |  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||||||
|  |  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  |  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||||
|  |  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package eu.siacs.conversations.utils; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Locale; | ||||||
|  | 
 | ||||||
|  | public class FtsUtils { | ||||||
|  | 
 | ||||||
|  | 	private static List<String> KEYWORDS = Arrays.asList("OR", "AND"); | ||||||
|  | 
 | ||||||
|  | 	public static List<String> parse(String input) { | ||||||
|  | 		List<String> term = new ArrayList<>(); | ||||||
|  | 		for (String part : input.split("\\s+")) { | ||||||
|  | 			if (part.isEmpty()) { | ||||||
|  | 				continue; | ||||||
|  | 			} | ||||||
|  | 			final String cleaned = part.substring(getStartIndex(part), getEndIndex(part) +1); | ||||||
|  | 			if (isKeyword(cleaned)) { | ||||||
|  | 				term.add(part); | ||||||
|  | 			} else { | ||||||
|  | 				term.add(cleaned); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return term; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static String toMatchString(List<String> terms) { | ||||||
|  | 		StringBuilder builder = new StringBuilder(); | ||||||
|  | 		for (String term : terms) { | ||||||
|  | 			if (builder.length() != 0) { | ||||||
|  | 				builder.append(' '); | ||||||
|  | 			} | ||||||
|  | 			if (isKeyword(term)) { | ||||||
|  | 				builder.append(term.toUpperCase(Locale.ENGLISH)); | ||||||
|  | 			} else if (term.contains("*") || term.startsWith("-")) { | ||||||
|  | 				builder.append(term); | ||||||
|  | 			} else { | ||||||
|  | 				builder.append('*').append(term).append('*'); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return builder.toString(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static boolean isKeyword(String term) { | ||||||
|  | 		return KEYWORDS.contains(term.toUpperCase(Locale.ENGLISH)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static int getStartIndex(String term) { | ||||||
|  | 		int index = 0; | ||||||
|  | 		while (term.charAt(index) == '*') { | ||||||
|  | 			++index; | ||||||
|  | 		} | ||||||
|  | 		return index; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static int getEndIndex(String term) { | ||||||
|  | 		int index = term.length() - 1; | ||||||
|  | 		while (term.charAt(index) == '*') { | ||||||
|  | 			--index; | ||||||
|  | 		} | ||||||
|  | 		return index; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -91,7 +91,15 @@ public class StylingHelper { | |||||||
| 		format(editable, end, editable.length() - 1, textColor); | 		format(editable, end, editable.length() - 1, textColor); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public static void highlight(final Context context, final Editable editable, String needle, boolean dark) { | 	public static void highlight(final Context context, final Editable editable, List<String> needles, boolean dark) { | ||||||
|  | 		for(String needle : needles) { | ||||||
|  | 			if (!FtsUtils.isKeyword(needle)) { | ||||||
|  | 				highlight(context, editable, needle, dark); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static void highlight(final Context context, final Editable editable, String needle, boolean dark) { | ||||||
| 		final int length = needle.length(); | 		final int length = needle.length(); | ||||||
| 		String string = editable.toString(); | 		String string = editable.toString(); | ||||||
| 		int start = indexOfIgnoreCase(string, needle, 0); | 		int start = indexOfIgnoreCase(string, needle, 0); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Daniel Gultsch
						Daniel Gultsch