properly cancel pending searchs and scroll to bottom after refresh

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/Config.java                                    |  1 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java               |  2 
src/main/java/eu/siacs/conversations/services/MessageSearchTask.java                | 59 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java                   | 15 
src/main/java/eu/siacs/conversations/ui/SearchActivity.java                         | 10 
src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java                     | 57 
src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java |  7 
src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java          | 33 
8 files changed, 140 insertions(+), 44 deletions(-)

Detailed changes

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;
 

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+'%'});
 	}
 

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<String,Conversational> conversationCache = new HashMap<>();
+			final HashMap<String, Conversational> conversationCache = new HashMap<>();
 			final List<Message> 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();
-	}
 }

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

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

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

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();
+			}
+		}
+	}
 }