From 3e5e3dd1bc8a35f55c57f807a97e40c9dd419a9a Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 12 May 2019 14:28:18 +0200 Subject: [PATCH] Redesigned search --- FAQ.md | 12 +- .../java/eu/faircode/email/ActivityView.java | 5 +- .../email/BoundaryCallbackMessages.java | 139 +++++++++--------- .../eu/faircode/email/FragmentAccounts.java | 5 +- .../eu/faircode/email/FragmentContacts.java | 1 + .../eu/faircode/email/FragmentFolders.java | 5 +- .../eu/faircode/email/FragmentMessages.java | 100 +++++++++++-- app/src/main/res/layout/fragment_messages.xml | 11 ++ 8 files changed, 193 insertions(+), 85 deletions(-) diff --git a/FAQ.md b/FAQ.md index f5a90f2800..0a348207b5 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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 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. -Searching on the server will be in the current folder -or the archive folder of the primary account if there is no current folder, for example when searching from the unified inbox. +Messages will be searched on the device first (all accounts, all folders). +There will be an action button with a cloud download icon at the bottom to search on the server. +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. -Note that it is possible that searching on the server will find messages newer than already found locally. +The IMAP protocol doesn't support searching in more than one folder at the same time. +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. The message text of local messages will not be searched if the message text was not downloaded yet. diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 73f4d82e92..28b311522e 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -501,9 +501,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB setIntent(intent); FragmentMessages.search( - ActivityView.this, ActivityView.this, - getSupportFragmentManager(), - -1, search); + ActivityView.this, ActivityView.this, getSupportFragmentManager(), + -1, false, search); } } }; diff --git a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java index 268f6159a4..48f358b4c1 100644 --- a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java +++ b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java @@ -69,23 +69,25 @@ import javax.mail.search.SubjectTerm; public class BoundaryCallbackMessages extends PagedList.BoundaryCallback { private Context context; private Long folder; - private String searching; + private boolean server; + private String query; private int pageSize; private IBoundaryCallbackMessages intf; private Handler handler; private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); + private boolean destroyed = false; private boolean error = false; - private int local_index = 0; - private int remote_index = -1; + private int index = 0; + private boolean loading = false; + private List messages = null; + private IMAPStore istore = null; private IMAPFolder ifolder = null; private Message[] imessages = null; - private boolean loading = false; - interface IBoundaryCallbackMessages { void onLoading(); @@ -96,12 +98,13 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback 0 && responses[responses.length - 1].isOK()) { List msgnums = new ArrayList<>(); @@ -353,7 +360,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback>>() { + @Override + protected Map> onExecute(Context context, Bundle args) { + Map> result = new LinkedHashMap<>(); + + DB db = DB.getInstance(context); + List accounts = db.account().getSynchronizingAccounts(); + + for (EntityAccount account : accounts) { + List 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> 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() { @Override public void onClick(View v) { @@ -597,6 +668,10 @@ public class FragmentMessages extends FragmentBase { pbWait.setVisibility(View.VISIBLE); fab.hide(); + if (viewType == AdapterMessage.ViewType.SEARCH && !server) + fabSearch.show(); + else + fabSearch.hide(); fabMore.hide(); fabError.hide(); @@ -1949,7 +2024,7 @@ public class FragmentMessages extends FragmentBase { break; case SEARCH: - setSubtitle(getString(R.string.title_searching, search)); + setSubtitle(getString(R.string.title_searching, query)); break; } @@ -1960,7 +2035,7 @@ public class FragmentMessages extends FragmentBase { else 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(), new Observer>() { @Override @@ -2104,6 +2179,7 @@ public class FragmentMessages extends FragmentBase { final MenuItem menuSearch = menu.findItem(R.id.menu_search); SearchView searchView = (SearchView) menuSearch.getActionView(); + searchView.setQueryHint(getString(R.string.title_search)); if (!TextUtils.isEmpty(searching)) { menuSearch.expandActionView(); @@ -2121,7 +2197,9 @@ public class FragmentMessages extends FragmentBase { public boolean onQueryTextSubmit(String query) { searching = null; menuSearch.collapseActionView(); - search(getContext(), getViewLifecycleOwner(), getFragmentManager(), folder, query); + search( + getContext(), getViewLifecycleOwner(), getFragmentManager(), + folder, false, query); return true; } }); @@ -2478,7 +2556,7 @@ public class FragmentMessages extends FragmentBase { if (boundaryCallback == null) boundaryCallback = new BoundaryCallbackMessages( getContext(), getViewLifecycleOwner(), - folder, search, REMOTE_PAGE_SIZE, + folder, server, query, REMOTE_PAGE_SIZE, new BoundaryCallbackMessages.IBoundaryCallbackMessages() { @Override public void onLoading() { @@ -3122,11 +3200,12 @@ public class FragmentMessages extends FragmentBase { static void search( final Context context, final LifecycleOwner owner, final FragmentManager manager, - long folder, String query) { + long folder, boolean server, String query) { if (Helper.isPro(context)) { Bundle args = new Bundle(); args.putLong("folder", folder); - args.putString("search", query); + args.putBoolean("server", server); + args.putString("query", query); new SimpleTask() { @Override @@ -3137,6 +3216,9 @@ public class FragmentMessages extends FragmentBase { @Override 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(); fragment.setArguments(args); diff --git a/app/src/main/res/layout/fragment_messages.xml b/app/src/main/res/layout/fragment_messages.xml index 84cc62afed..29d8055a34 100644 --- a/app/src/main/res/layout/fragment_messages.xml +++ b/app/src/main/res/layout/fragment_messages.xml @@ -244,6 +244,17 @@ android:tint="@color/colorActionForeground" app:backgroundTint="?attr/colorAccent" /> + +