From e6feb91390dfd43e2c7a8443b17139d14a425938 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 26 Apr 2018 17:02:31 +0200 Subject: [PATCH] properly cancel pending searchs and scroll to bottom after refresh --- .../java/eu/siacs/conversations/Config.java | 1 + .../persistance/DatabaseBackend.java | 2 +- .../services/MessageSearchTask.java | 59 ++++++++++++------- .../ui/ConversationFragment.java | 15 +---- .../conversations/ui/SearchActivity.java | 10 +++- .../conversations/ui/util/ListViewUtils.java | 57 ++++++++++++++++++ .../ReplacingSerialSingleThreadExecutor.java | 7 +++ .../utils/SerialSingleThreadExecutor.java | 33 ++++++++--- 8 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index e128ef358..db4009910 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -74,6 +74,7 @@ public final class Config { public static final int PAGE_SIZE = 50; public static final int MAX_NUM_PAGES = 3; + public static final int MAX_SEARCH_RESULTS = 300; public static final int REFRESH_UI_INTERVAL = 500; diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 01d7e486b..f7be6133c 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -706,7 +706,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public Cursor getMessageSearchCursor(String term) { 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+" where "+Message.BODY +" LIKE ? limit 200"; + 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+" where "+Message.ENCRYPTION+" NOT IN("+Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE+','+Message.ENCRYPTION_PGP+','+Message.ENCRYPTION_DECRYPTION_FAILED+") AND "+Message.BODY +" LIKE ? ORDER BY "+Message.TIME_SENT+" DESC limit "+Config.MAX_SEARCH_RESULTS; return db.rawQuery(SQL,new String[]{'%'+term+'%'}); } diff --git a/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java b/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java index 20cdec351..416e54b9b 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java +++ b/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java @@ -66,6 +66,14 @@ public class MessageSearchTask implements Runnable, Cancellable { this.onSearchResultsAvailable = onSearchResultsAvailable; } + public static void search(XmppConnectionService xmppConnectionService, String term, OnSearchResultsAvailable onSearchResultsAvailable) { + new MessageSearchTask(xmppConnectionService, term, onSearchResultsAvailable).executeInBackground(); + } + + public static void cancelRunningTasks() { + EXECUTOR.cancelRunningTasks(); + } + @Override public void cancel() { this.isCancelled = true; @@ -76,29 +84,40 @@ public class MessageSearchTask implements Runnable, Cancellable { long startTimestamp = SystemClock.elapsedRealtime(); Cursor cursor = null; try { - final HashMap conversationCache = new HashMap<>(); + final HashMap conversationCache = new HashMap<>(); final List result = new ArrayList<>(); cursor = xmppConnectionService.databaseBackend.getMessageSearchCursor(term); - while(cursor.moveToNext()) { - final String conversationUuid = cursor.getString(cursor.getColumnIndex(Message.CONVERSATION)); - Conversational conversation; - if (conversationCache.containsKey(conversationUuid)) { - conversation = conversationCache.get(conversationUuid); - } else { - String accountUuid = cursor.getString(cursor.getColumnIndex(Conversation.ACCOUNT)); - String contactJid = cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)); - int mode = cursor.getInt(cursor.getColumnIndex(Conversation.MODE)); - conversation = findOrGenerateStub(conversationUuid, accountUuid, contactJid, mode); - conversationCache.put(conversationUuid, conversation); - } - Message message = IndividualMessage.fromCursor(cursor, conversation); - result.add(message); + if (isCancelled) { + Log.d(Config.LOGTAG, "canceled search task"); + return; + } + if (cursor != null && cursor.getCount() > 0) { + cursor.moveToLast(); + do { + if (isCancelled) { + Log.d(Config.LOGTAG, "canceled search task"); + return; + } + final String conversationUuid = cursor.getString(cursor.getColumnIndex(Message.CONVERSATION)); + Conversational conversation; + if (conversationCache.containsKey(conversationUuid)) { + conversation = conversationCache.get(conversationUuid); + } else { + String accountUuid = cursor.getString(cursor.getColumnIndex(Conversation.ACCOUNT)); + String contactJid = cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)); + int mode = cursor.getInt(cursor.getColumnIndex(Conversation.MODE)); + conversation = findOrGenerateStub(conversationUuid, accountUuid, contactJid, mode); + conversationCache.put(conversationUuid, conversation); + } + Message message = IndividualMessage.fromCursor(cursor, conversation); + result.add(message); + } while (cursor.moveToPrevious()); } long stopTimestamp = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG,"found "+result.size()+" messages in "+(stopTimestamp - startTimestamp)+"ms"); + Log.d(Config.LOGTAG, "found " + result.size() + " messages in " + (stopTimestamp - startTimestamp) + "ms"); onSearchResultsAvailable.onSearchResultsAvailable(term, result); } catch (Exception e) { - Log.d(Config.LOGTAG,"exception while searching ",e); + Log.d(Config.LOGTAG, "exception while searching ", e); } finally { if (cursor != null) { cursor.close(); @@ -116,14 +135,10 @@ public class MessageSearchTask implements Runnable, Cancellable { if (account != null && jid != null) { return new StubConversation(account, conversationUuid, jid.asBareJid(), mode); } - throw new Exception("Unable to generate stub for "+contactJid); + throw new Exception("Unable to generate stub for " + contactJid); } private void executeInBackground() { EXECUTOR.execute(this); } - - public static void search(XmppConnectionService xmppConnectionService, String term, OnSearchResultsAvailable onSearchResultsAvailable) { - new MessageSearchTask(xmppConnectionService, term, onSearchResultsAvailable).executeInBackground(); - } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 3e127c690..904c12481 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -85,6 +85,7 @@ import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.util.ActivityResult; import eu.siacs.conversations.ui.util.AttachmentTool; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; +import eu.siacs.conversations.ui.util.ListViewUtils; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PresenceSelector; @@ -1940,21 +1941,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void setSelection(int pos, boolean jumpToBottom) { - setSelection(this.binding.messagesView, pos, jumpToBottom); - this.binding.messagesView.post(() -> setSelection(this.binding.messagesView, pos, jumpToBottom)); + ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom); + this.binding.messagesView.post(() -> ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom)); this.binding.messagesView.post(this::fireReadEvent); } - private static void setSelection(final ListView listView, int pos, boolean jumpToBottom) { - if (jumpToBottom) { - final View lastChild = listView.getChildAt(listView.getChildCount() - 1); - if (lastChild != null) { - listView.setSelectionFromTop(pos, -lastChild.getHeight()); - return; - } - } - listView.setSelection(pos); - } private boolean scrolledToBottom() { return this.binding != null && scrolledToBottom(this.binding.messagesView); diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index b9731d669..a481b8173 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -33,6 +33,7 @@ import android.databinding.DataBindingUtil; import android.os.Bundle; import android.support.v7.widget.Toolbar; import android.text.Editable; +import android.text.InputType; import android.text.TextWatcher; import android.util.Log; import android.view.Menu; @@ -46,10 +47,12 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivitySearchBinding; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.MessageSearchTask; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; import eu.siacs.conversations.ui.util.Color; import eu.siacs.conversations.ui.util.Drawable; +import eu.siacs.conversations.ui.util.ListViewUtils; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard; @@ -77,6 +80,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc EditText searchField = searchActionMenuItem.getActionView().findViewById(R.id.search_field); searchField.addTextChangedListener(this); searchField.setHint(R.string.search_messages); + searchField.setInputType(InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); showKeyboard(searchField); return super.onCreateOptionsMenu(menu); } @@ -127,6 +131,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc if (term.length() > 0) { xmppConnectionService.search(s.toString().trim(), this); } else { + MessageSearchTask.cancelRunningTasks(); this.messages.clear(); messageListAdapter.notifyDataSetChanged(); changeBackground(false, false); @@ -135,11 +140,12 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc @Override public void onSearchResultsAvailable(String term, List messages) { - this.messages.clear(); - this.messages.addAll(messages); runOnUiThread(() -> { + this.messages.clear(); + this.messages.addAll(messages); messageListAdapter.notifyDataSetChanged(); changeBackground(true, messages.size() > 0); + ListViewUtils.scrollToBottom(this.binding.searchResults); }); } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java b/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java new file mode 100644 index 000000000..63317b6b8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java @@ -0,0 +1,57 @@ +/* + * 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.ui.util; + +import android.view.View; +import android.widget.ListView; + + +public class ListViewUtils { + + public static void scrollToBottom(final ListView listView) { + int count = listView.getAdapter().getCount(); + if (count > 0) { + setSelection(listView, count - 1, true); + } + } + + public static void setSelection(final ListView listView, int pos, boolean jumpToBottom) { + if (jumpToBottom) { + final View lastChild = listView.getChildAt(listView.getChildCount() - 1); + if (lastChild != null) { + listView.setSelectionFromTop(pos, -lastChild.getHeight()); + return; + } + } + listView.setSelection(pos); + } + +} + diff --git a/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java index 601b995b8..c42c1ab1a 100644 --- a/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java +++ b/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java @@ -18,4 +18,11 @@ public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecu } super.execute(r); } + + public synchronized void cancelRunningTasks() { + tasks.clear(); + if (active != null && active instanceof Cancellable) { + ((Cancellable) active).cancel(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java index 768fc7a67..0afe11430 100644 --- a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java +++ b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java @@ -30,13 +30,7 @@ public class SerialSingleThreadExecutor implements Executor { } public synchronized void execute(final Runnable r) { - tasks.offer(() -> { - try { - r.run(); - } finally { - scheduleNext(); - } - }); + tasks.offer(new Runner(r)); if (active == null) { scheduleNext(); } @@ -51,4 +45,29 @@ public class SerialSingleThreadExecutor implements Executor { } } } + + private class Runner implements Runnable, Cancellable { + + private final Runnable runnable; + + private Runner(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void cancel() { + if (runnable instanceof Cancellable) { + ((Cancellable) runnable).cancel(); + } + } + + @Override + public void run() { + try { + runnable.run(); + } finally { + scheduleNext(); + } + } + } } \ No newline at end of file