Redesigned search

This commit is contained in:
M66B 2019-05-12 14:28:18 +02:00
parent 215a28965e
commit 3e5e3dd1bc
8 changed files with 193 additions and 85 deletions

12
FAQ.md
View File

@ -423,12 +423,14 @@ about [these vulnerabilities](https://amp.thehackernews.com/thn/2019/04/email-si
You can start searching for messages on sender, recipient, subject, keyword or message text by using the magnify glass in the action bar of a folder. You can start searching for messages on sender, recipient, subject, keyword or message text by using the magnify glass in the action bar of a folder.
You can also search from any app by select *Search email* in the copy/paste popup menu. You can also search from any app by select *Search email* in the copy/paste popup menu.
First on device messages will be searched and after that the search will be executed on the server. Messages will be searched on the device first (all accounts, all folders).
Searching on the server will be in the current folder There will be an action button with a cloud download icon at the bottom to search on the server.
or the archive folder of the primary account if there is no current folder, for example when searching from the unified inbox. When the search was started in a specific folder,
the same folder will be searched in on the server,
else you can select which folder to search in on the server.
Searching on the server will be triggered/continued when reaching the end of the list. The IMAP protocol doesn't support searching in more than one folder at the same time.
Note that it is possible that searching on the server will find messages newer than already found locally. Searching on the server is an expensive operation, therefore it is not possible to select multiple folders.
Searching local messages is case insensitive and on partial text. Searching local messages is case insensitive and on partial text.
The message text of local messages will not be searched if the message text was not downloaded yet. The message text of local messages will not be searched if the message text was not downloaded yet.

View File

@ -501,9 +501,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
setIntent(intent); setIntent(intent);
FragmentMessages.search( FragmentMessages.search(
ActivityView.this, ActivityView.this, ActivityView.this, ActivityView.this, getSupportFragmentManager(),
getSupportFragmentManager(), -1, false, search);
-1, search);
} }
} }
}; };

View File

@ -69,23 +69,25 @@ import javax.mail.search.SubjectTerm;
public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMessageEx> { public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMessageEx> {
private Context context; private Context context;
private Long folder; private Long folder;
private String searching; private boolean server;
private String query;
private int pageSize; private int pageSize;
private IBoundaryCallbackMessages intf; private IBoundaryCallbackMessages intf;
private Handler handler; private Handler handler;
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private boolean destroyed = false;
private boolean error = false; private boolean error = false;
private int local_index = 0; private int index = 0;
private int remote_index = -1; private boolean loading = false;
private List<Long> messages = null; private List<Long> messages = null;
private IMAPStore istore = null; private IMAPStore istore = null;
private IMAPFolder ifolder = null; private IMAPFolder ifolder = null;
private Message[] imessages = null; private Message[] imessages = null;
private boolean loading = false;
interface IBoundaryCallbackMessages { interface IBoundaryCallbackMessages {
void onLoading(); void onLoading();
@ -96,12 +98,13 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
BoundaryCallbackMessages( BoundaryCallbackMessages(
Context context, LifecycleOwner owner, Context context, LifecycleOwner owner,
long folder, String searching, int pageSize, long folder, boolean server, String query, int pageSize,
IBoundaryCallbackMessages intf) { IBoundaryCallbackMessages intf) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.folder = (folder < 0 ? null : folder); this.folder = (folder < 0 ? null : folder);
this.searching = searching; this.server = server;
this.query = query;
this.pageSize = pageSize; this.pageSize = pageSize;
this.intf = intf; this.intf = intf;
@ -110,6 +113,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
owner.getLifecycle().addObserver(new LifecycleObserver() { owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() { public void onDestroyed() {
destroyed = true;
executor.submit(new Runnable() { executor.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -128,13 +133,13 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
@Override @Override
public void onZeroItemsLoaded() { public void onZeroItemsLoaded() {
Log.i("onZeroItemsLoaded"); Log.i("Boundary zero loaded");
queue_load(); queue_load();
} }
@Override @Override
public void onItemAtEndLoaded(final TupleMessageEx itemAtEnd) { public void onItemAtEndLoaded(final TupleMessageEx itemAtEnd) {
Log.i("onItemAtEndLoaded"); Log.i("Boundary at end");
queue_load(); queue_load();
} }
@ -145,6 +150,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
@Override @Override
public void run() { public void run() {
try { try {
if (destroyed)
return;
loading = true; loading = true;
fetched = 0; fetched = 0;
handler.post(new Runnable() { handler.post(new Runnable() {
@ -153,7 +161,10 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
intf.onLoading(); intf.onLoading();
} }
}); });
fetched = load(); if (server)
fetched = load_server();
else
fetched = load_device();
} catch (final Throwable ex) { } catch (final Throwable ex) {
Log.e("Boundary", ex); Log.e("Boundary", ex);
handler.post(new Runnable() { handler.post(new Runnable() {
@ -179,32 +190,28 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return loading; return loading;
} }
private int load() throws MessagingException, IOException, AuthenticatorException, OperationCanceledException { private int load_device() {
if (error)
return 0;
DB db = DB.getInstance(context); DB db = DB.getInstance(context);
// Search local int found = 0;
int local_count = 0;
try { try {
db.beginTransaction(); db.beginTransaction();
if (messages == null) { if (messages == null) {
messages = db.message().getMessageIdsByFolder(folder); messages = db.message().getMessageIdsByFolder(folder);
Log.i("Boundary search folder=" + folder + " messages=" + messages.size()); Log.i("Boundary device folder=" + folder + " query=" + query + " messages=" + messages.size());
} }
for (int i = local_index; i < messages.size() && local_count < pageSize; i++) { for (int i = index; i < messages.size() && found < pageSize && !destroyed; i++) {
local_index = i + 1; index = i + 1;
EntityMessage message = db.message().getMessage(messages.get(i)); EntityMessage message = db.message().getMessage(messages.get(i));
boolean match = false; boolean match = false;
if (searching == null) if (query == null)
match = true; match = true;
else { else {
String find = searching.toLowerCase(); String find = query.toLowerCase();
String body = null; String body = null;
if (message.content) if (message.content)
try { try {
@ -229,36 +236,36 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
} }
if (match) { if (match) {
local_count++; found++;
db.message().setMessageFound(message.account, message.thread); db.message().setMessageFound(message.account, message.thread);
} }
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();
if (local_count == pageSize) if (found == pageSize)
return local_count; return found;
} finally { } finally {
db.endTransaction(); db.endTransaction();
} }
// Search remote Log.i("Boundary device done");
long bid; return found;
if (folder == null) { }
EntityFolder archive = db.folder().getPrimaryArchive();
if (archive == null)
return local_count;
else
bid = archive.id;
} else
bid = folder;
final EntityFolder browsable = db.folder().getBrowsableFolder(bid, searching != null); private int load_server() throws MessagingException, IOException, AuthenticatorException, OperationCanceledException {
DB db = DB.getInstance(context);
if (destroyed || error)
return 0;
final EntityFolder browsable = db.folder().getBrowsableFolder(folder, query != null);
if (browsable == null) if (browsable == null)
return local_count; return 0;
EntityAccount account = db.account().getAccount(browsable.account); EntityAccount account = db.account().getAccount(browsable.account);
if (account == null) if (account == null)
return local_count; return 0;
if (imessages == null) if (imessages == null)
try { try {
@ -279,16 +286,16 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
Session isession = Session.getInstance(props, null); Session isession = Session.getInstance(props, null);
isession.setDebug(debug); isession.setDebug(debug);
Log.i("Boundary connecting account=" + account.name); Log.i("Boundary server connecting account=" + account.name);
istore = (IMAPStore) isession.getStore(protocol); istore = (IMAPStore) isession.getStore(protocol);
Helper.connect(context, istore, account); Helper.connect(context, istore, account);
Log.i("Boundary opening folder=" + browsable.name); Log.i("Boundary server opening folder=" + browsable.name);
ifolder = (IMAPFolder) istore.getFolder(browsable.name); ifolder = (IMAPFolder) istore.getFolder(browsable.name);
ifolder.open(Folder.READ_WRITE); ifolder.open(Folder.READ_WRITE);
Log.i("Boundary searching=" + searching); Log.i("Boundary server query=" + query);
if (searching == null) if (query == null)
imessages = ifolder.getMessages(); imessages = ifolder.getMessages();
else { else {
Object result = ifolder.doCommand(new IMAPFolder.ProtocolCommand() { Object result = ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
@ -305,11 +312,11 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
try { try {
// https://tools.ietf.org/html/rfc3501#section-6.4.4 // https://tools.ietf.org/html/rfc3501#section-6.4.4
Argument arg = new Argument(); Argument arg = new Argument();
if (searching.startsWith("raw:") && istore.hasCapability("X-GM-EXT-1")) { if (query.startsWith("raw:") && istore.hasCapability("X-GM-EXT-1")) {
// https://support.google.com/mail/answer/7190 // https://support.google.com/mail/answer/7190
// https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw // https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw
arg.writeAtom("X-GM-RAW"); arg.writeAtom("X-GM-RAW");
arg.writeString(searching.substring(4)); arg.writeString(query.substring(4));
} else { } else {
if (!protocol.supportsUtf8()) { if (!protocol.supportsUtf8()) {
arg.writeAtom("CHARSET"); arg.writeAtom("CHARSET");
@ -321,20 +328,20 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
arg.writeAtom("OR"); arg.writeAtom("OR");
arg.writeAtom("OR"); arg.writeAtom("OR");
arg.writeAtom("FROM"); arg.writeAtom("FROM");
arg.writeBytes(searching.getBytes()); arg.writeBytes(query.getBytes());
arg.writeAtom("TO"); arg.writeAtom("TO");
arg.writeBytes(searching.getBytes()); arg.writeBytes(query.getBytes());
arg.writeAtom("SUBJECT"); arg.writeAtom("SUBJECT");
arg.writeBytes(searching.getBytes()); arg.writeBytes(query.getBytes());
arg.writeAtom("BODY"); arg.writeAtom("BODY");
arg.writeBytes(searching.getBytes()); arg.writeBytes(query.getBytes());
if (keywords) { if (keywords) {
arg.writeAtom("KEYWORD"); arg.writeAtom("KEYWORD");
arg.writeBytes(searching.getBytes()); arg.writeBytes(query.getBytes());
} }
} }
Log.i("Boundary UTF8 search=" + searching); Log.i("Boundary UTF8 search=" + query);
Response[] responses = protocol.command("SEARCH", arg); Response[] responses = protocol.command("SEARCH", arg);
if (responses.length > 0 && responses[responses.length - 1].isOK()) { if (responses.length > 0 && responses[responses.length - 1].isOK()) {
List<Integer> msgnums = new ArrayList<>(); List<Integer> msgnums = new ArrayList<>();
@ -353,7 +360,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return imessages; return imessages;
} else { } else {
// Assume no UTF-8 support // Assume no UTF-8 support
String search = searching.replace("ß", "ss"); // Eszett String search = query.replace("ß", "ss"); // Eszett
search = Normalizer.normalize(search, Normalizer.Form.NFD) search = Normalizer.normalize(search, Normalizer.Form.NFD)
.replaceAll("[^\\p{ASCII}]", ""); .replaceAll("[^\\p{ASCII}]", "");
@ -388,9 +395,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
imessages = (Message[]) result; imessages = (Message[]) result;
} }
Log.i("Boundary found messages=" + imessages.length); Log.i("Boundary server found messages=" + imessages.length);
remote_index = imessages.length - 1; index = imessages.length - 1;
} catch (Throwable ex) { } catch (Throwable ex) {
error = true; error = true;
if (ex instanceof FolderClosedException) if (ex instanceof FolderClosedException)
@ -401,12 +408,12 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
} }
} }
int remote_count = 0; int found = 0;
while (remote_index >= 0 && remote_count < pageSize) { while (index >= 0 && found < pageSize && !destroyed) {
Log.i("Boundary index=" + remote_index); Log.i("Boundary server index=" + index);
int from = Math.max(0, remote_index - (pageSize - remote_count) + 1); int from = Math.max(0, index - (pageSize - found) + 1);
Message[] isub = Arrays.copyOfRange(imessages, from, remote_index + 1); Message[] isub = Arrays.copyOfRange(imessages, from, index + 1);
remote_index -= (pageSize - remote_count); index -= (pageSize - found);
FetchProfile fp = new FetchProfile(); FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.ENVELOPE);
@ -421,10 +428,10 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
try { try {
db.beginTransaction(); db.beginTransaction();
for (int j = isub.length - 1; j >= 0; j--) for (int j = isub.length - 1; j >= 0 && found < pageSize && !destroyed; j--)
try { try {
long uid = ifolder.getUID(isub[j]); long uid = ifolder.getUID(isub[j]);
Log.i("Boundary sync uid=" + uid); Log.i("Boundary server sync uid=" + uid);
EntityMessage message = db.message().getMessageByUid(browsable.id, uid); EntityMessage message = db.message().getMessageByUid(browsable.id, uid);
if (message == null) { if (message == null) {
message = Core.synchronizeMessage(context, message = Core.synchronizeMessage(context,
@ -432,21 +439,21 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
ifolder, (IMAPMessage) isub[j], ifolder, (IMAPMessage) isub[j],
true, true,
new ArrayList<EntityRule>()); new ArrayList<EntityRule>());
remote_count++; found++;
} }
db.message().setMessageFound(message.account, message.thread); db.message().setMessageFound(message.account, message.thread);
} catch (MessageRemovedException ex) { } catch (MessageRemovedException ex) {
Log.w(browsable.name + " boundary", ex); Log.w(browsable.name + " boundary server", ex);
} catch (FolderClosedException ex) { } catch (FolderClosedException ex) {
throw ex; throw ex;
} catch (IOException ex) { } catch (IOException ex) {
if (ex.getCause() instanceof MessagingException) { if (ex.getCause() instanceof MessagingException) {
Log.w(browsable.name + " boundary", ex); Log.w(browsable.name + " boundary server", ex);
db.folder().setFolderError(browsable.id, Helper.formatThrowable(ex, true)); db.folder().setFolderError(browsable.id, Helper.formatThrowable(ex, true));
} else } else
throw ex; throw ex;
} catch (Throwable ex) { } catch (Throwable ex) {
Log.e(browsable.name + " boundary", ex); Log.e(browsable.name + " boundary server", ex);
db.folder().setFolderError(browsable.id, Helper.formatThrowable(ex, true)); db.folder().setFolderError(browsable.id, Helper.formatThrowable(ex, true));
} finally { } finally {
((IMAPMessage) isub[j]).invalidateHeaders(); ((IMAPMessage) isub[j]).invalidateHeaders();
@ -458,7 +465,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
} }
} }
Log.i("Boundary done"); Log.i("Boundary server done");
return local_count + remote_count; return found;
} }
} }

View File

@ -222,6 +222,7 @@ public class FragmentAccounts extends FragmentBase {
final MenuItem menuSearch = menu.findItem(R.id.menu_search); final MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchView searchView = (SearchView) menuSearch.getActionView(); SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getString(R.string.title_search));
if (!TextUtils.isEmpty(searching)) { if (!TextUtils.isEmpty(searching)) {
menuSearch.expandActionView(); menuSearch.expandActionView();
@ -239,7 +240,9 @@ public class FragmentAccounts extends FragmentBase {
public boolean onQueryTextSubmit(String query) { public boolean onQueryTextSubmit(String query) {
searching = null; searching = null;
menuSearch.collapseActionView(); menuSearch.collapseActionView();
FragmentMessages.search(getContext(), getViewLifecycleOwner(), getFragmentManager(), -1, query); FragmentMessages.search(
getContext(), getViewLifecycleOwner(), getFragmentManager(),
-1, false, query);
return true; return true;
} }
}); });

View File

@ -120,6 +120,7 @@ public class FragmentContacts extends FragmentBase {
MenuItem menuSearch = menu.findItem(R.id.menu_search); MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchView searchView = (SearchView) menuSearch.getActionView(); SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getString(R.string.title_search));
if (!TextUtils.isEmpty(searching)) { if (!TextUtils.isEmpty(searching)) {
menuSearch.expandActionView(); menuSearch.expandActionView();

View File

@ -390,6 +390,7 @@ public class FragmentFolders extends FragmentBase {
final MenuItem menuSearch = menu.findItem(R.id.menu_search); final MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchView searchView = (SearchView) menuSearch.getActionView(); SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getString(R.string.title_search));
if (!TextUtils.isEmpty(searching)) { if (!TextUtils.isEmpty(searching)) {
menuSearch.expandActionView(); menuSearch.expandActionView();
@ -407,7 +408,9 @@ public class FragmentFolders extends FragmentBase {
public boolean onQueryTextSubmit(String query) { public boolean onQueryTextSubmit(String query) {
searching = null; searching = null;
menuSearch.collapseActionView(); menuSearch.collapseActionView();
FragmentMessages.search(getContext(), getViewLifecycleOwner(), getFragmentManager(), -1, query); FragmentMessages.search(
getContext(), getViewLifecycleOwner(), getFragmentManager(),
-1, false, query);
return true; return true;
} }
}); });

View File

@ -100,6 +100,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -131,14 +132,16 @@ public class FragmentMessages extends FragmentBase {
private Group grpReady; private Group grpReady;
private FloatingActionButton fab; private FloatingActionButton fab;
private FloatingActionButton fabMore; private FloatingActionButton fabMore;
private FloatingActionButton fabSearch;
private FloatingActionButton fabError; private FloatingActionButton fabError;
private long account; private long account;
private long folder; private long folder;
private boolean server;
private String thread; private String thread;
private long id; private long id;
private boolean found; private boolean found;
private String search; private String query;
private boolean pane; private boolean pane;
private boolean date; private boolean date;
@ -221,15 +224,16 @@ public class FragmentMessages extends FragmentBase {
Bundle args = getArguments(); Bundle args = getArguments();
account = args.getLong("account", -1); account = args.getLong("account", -1);
folder = args.getLong("folder", -1); folder = args.getLong("folder", -1);
server = args.getBoolean("server", false);
thread = args.getString("thread"); thread = args.getString("thread");
id = args.getLong("id", -1); id = args.getLong("id", -1);
found = args.getBoolean("found", false); found = args.getBoolean("found", false);
search = args.getString("search"); query = args.getString("query");
pane = args.getBoolean("pane", false); pane = args.getBoolean("pane", false);
primary = args.getLong("primary", -1); primary = args.getLong("primary", -1);
connected = args.getBoolean("connected", false); connected = args.getBoolean("connected", false);
if (TextUtils.isEmpty(search)) if (TextUtils.isEmpty(query))
if (thread == null) if (thread == null)
if (folder < 0) if (folder < 0)
viewType = AdapterMessage.ViewType.UNIFIED; viewType = AdapterMessage.ViewType.UNIFIED;
@ -286,6 +290,7 @@ public class FragmentMessages extends FragmentBase {
grpHintSelect = view.findViewById(R.id.grpHintSelect); grpHintSelect = view.findViewById(R.id.grpHintSelect);
grpReady = view.findViewById(R.id.grpReady); grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab); fab = view.findViewById(R.id.fab);
fabSearch = view.findViewById(R.id.fabSearch);
fabMore = view.findViewById(R.id.fabMore); fabMore = view.findViewById(R.id.fabMore);
fabError = view.findViewById(R.id.fabError); fabError = view.findViewById(R.id.fabError);
@ -569,6 +574,72 @@ public class FragmentMessages extends FragmentBase {
} }
}); });
fabSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (folder < 0) {
Bundle args = new Bundle();
new SimpleTask<Map<EntityAccount, List<EntityFolder>>>() {
@Override
protected Map<EntityAccount, List<EntityFolder>> onExecute(Context context, Bundle args) {
Map<EntityAccount, List<EntityFolder>> result = new LinkedHashMap<>();
DB db = DB.getInstance(context);
List<EntityAccount> accounts = db.account().getSynchronizingAccounts();
for (EntityAccount account : accounts) {
List<EntityFolder> folders = db.folder().getFolders(account.id);
if (folders.size() > 0)
Collections.sort(folders, folders.get(0).getComparator(context));
result.put(account, folders);
}
return result;
}
@Override
protected void onExecuted(Bundle args, Map<EntityAccount, List<EntityFolder>> result) {
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(getContext(), getViewLifecycleOwner(), fabSearch);
int order = 1;
for (EntityAccount account : result.keySet()) {
SubMenu smenu = popupMenu.getMenu()
.addSubMenu(Menu.NONE, 0, order++, account.name);
int sorder = 1;
for (EntityFolder folder : result.get(account)) {
MenuItem item = smenu.add(Menu.NONE, 1, sorder++, folder.getDisplayName(getContext()));
item.setIntent(new Intent().putExtra("target", folder.id));
}
}
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {
Intent intent = target.getIntent();
if (intent == null)
return false;
long folder = intent.getLongExtra("target", -1);
search(getContext(), getViewLifecycleOwner(), getFragmentManager(), folder, true, query);
return true;
}
});
popupMenu.show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentMessages.this, args, "messages:search");
} else
search(getContext(), getViewLifecycleOwner(), getFragmentManager(), folder, true, query);
}
});
fabMore.setOnClickListener(new View.OnClickListener() { fabMore.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -597,6 +668,10 @@ public class FragmentMessages extends FragmentBase {
pbWait.setVisibility(View.VISIBLE); pbWait.setVisibility(View.VISIBLE);
fab.hide(); fab.hide();
if (viewType == AdapterMessage.ViewType.SEARCH && !server)
fabSearch.show();
else
fabSearch.hide();
fabMore.hide(); fabMore.hide();
fabError.hide(); fabError.hide();
@ -1949,7 +2024,7 @@ public class FragmentMessages extends FragmentBase {
break; break;
case SEARCH: case SEARCH:
setSubtitle(getString(R.string.title_searching, search)); setSubtitle(getString(R.string.title_searching, query));
break; break;
} }
@ -1960,7 +2035,7 @@ public class FragmentMessages extends FragmentBase {
else else
fabMore.hide(); fabMore.hide();
if (viewType != AdapterMessage.ViewType.THREAD) { if (viewType != AdapterMessage.ViewType.THREAD && viewType != AdapterMessage.ViewType.SEARCH) {
db.identity().liveComposableIdentities(account < 0 ? null : account).observe(getViewLifecycleOwner(), db.identity().liveComposableIdentities(account < 0 ? null : account).observe(getViewLifecycleOwner(),
new Observer<List<TupleIdentityEx>>() { new Observer<List<TupleIdentityEx>>() {
@Override @Override
@ -2104,6 +2179,7 @@ public class FragmentMessages extends FragmentBase {
final MenuItem menuSearch = menu.findItem(R.id.menu_search); final MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchView searchView = (SearchView) menuSearch.getActionView(); SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getString(R.string.title_search));
if (!TextUtils.isEmpty(searching)) { if (!TextUtils.isEmpty(searching)) {
menuSearch.expandActionView(); menuSearch.expandActionView();
@ -2121,7 +2197,9 @@ public class FragmentMessages extends FragmentBase {
public boolean onQueryTextSubmit(String query) { public boolean onQueryTextSubmit(String query) {
searching = null; searching = null;
menuSearch.collapseActionView(); menuSearch.collapseActionView();
search(getContext(), getViewLifecycleOwner(), getFragmentManager(), folder, query); search(
getContext(), getViewLifecycleOwner(), getFragmentManager(),
folder, false, query);
return true; return true;
} }
}); });
@ -2478,7 +2556,7 @@ public class FragmentMessages extends FragmentBase {
if (boundaryCallback == null) if (boundaryCallback == null)
boundaryCallback = new BoundaryCallbackMessages( boundaryCallback = new BoundaryCallbackMessages(
getContext(), getViewLifecycleOwner(), getContext(), getViewLifecycleOwner(),
folder, search, REMOTE_PAGE_SIZE, folder, server, query, REMOTE_PAGE_SIZE,
new BoundaryCallbackMessages.IBoundaryCallbackMessages() { new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
@Override @Override
public void onLoading() { public void onLoading() {
@ -3122,11 +3200,12 @@ public class FragmentMessages extends FragmentBase {
static void search( static void search(
final Context context, final LifecycleOwner owner, final FragmentManager manager, final Context context, final LifecycleOwner owner, final FragmentManager manager,
long folder, String query) { long folder, boolean server, String query) {
if (Helper.isPro(context)) { if (Helper.isPro(context)) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("folder", folder); args.putLong("folder", folder);
args.putString("search", query); args.putBoolean("server", server);
args.putString("query", query);
new SimpleTask<Void>() { new SimpleTask<Void>() {
@Override @Override
@ -3137,6 +3216,9 @@ public class FragmentMessages extends FragmentBase {
@Override @Override
protected void onExecuted(Bundle args, Void data) { protected void onExecuted(Bundle args, Void data) {
if (owner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
manager.popBackStack("search", FragmentManager.POP_BACK_STACK_INCLUSIVE);
FragmentMessages fragment = new FragmentMessages(); FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(args); fragment.setArguments(args);

View File

@ -244,6 +244,17 @@
android:tint="@color/colorActionForeground" android:tint="@color/colorActionForeground"
app:backgroundTint="?attr/colorAccent" /> app:backgroundTint="?attr/colorAccent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_padding"
android:src="@drawable/baseline_cloud_download_24"
android:tint="@color/colorActionForeground"
android:tooltipText="@string/title_search"
app:backgroundTint="?attr/colorAccent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"