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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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