Draft new search

pull/178/head
M66B 5 years ago
parent b0ae99b216
commit adb6d9658e

@ -25,10 +25,12 @@ import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagedList; import androidx.paging.PagedList;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.sun.mail.iap.Argument; import com.sun.mail.iap.Argument;
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response; import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage; import com.sun.mail.imap.IMAPMessage;
@ -38,11 +40,12 @@ import com.sun.mail.imap.protocol.IMAPResponse;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.io.Serializable;
import java.text.Normalizer; import java.text.Normalizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import javax.mail.FetchProfile; import javax.mail.FetchProfile;
@ -61,6 +64,7 @@ import javax.mail.search.FlagTerm;
import javax.mail.search.FromStringTerm; import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm; import javax.mail.search.OrTerm;
import javax.mail.search.RecipientStringTerm; import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SearchException;
import javax.mail.search.SearchTerm; import javax.mail.search.SearchTerm;
import javax.mail.search.SubjectTerm; import javax.mail.search.SubjectTerm;
@ -71,7 +75,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private Long account; private Long account;
private Long folder; private Long folder;
private boolean server; private boolean server;
private String query; private SearchCriteria criteria;
private int pageSize; private int pageSize;
private IBoundaryCallbackMessages intf; private IBoundaryCallbackMessages intf;
@ -91,12 +95,12 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
void onException(@NonNull Throwable ex); void onException(@NonNull Throwable ex);
} }
BoundaryCallbackMessages(Context context, long account, long folder, boolean server, String query, int pageSize) { BoundaryCallbackMessages(Context context, long account, long folder, boolean server, SearchCriteria criteria, int pageSize) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.account = (account < 0 ? null : account); this.account = (account < 0 ? null : account);
this.folder = (folder < 0 ? null : folder); this.folder = (folder < 0 ? null : folder);
this.server = server; this.server = server;
this.query = query; this.criteria = criteria;
this.pageSize = pageSize; this.pageSize = pageSize;
} }
@ -182,35 +186,15 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private int load_device(State state) { private int load_device(State state) {
DB db = DB.getInstance(context); DB db = DB.getInstance(context);
Boolean seen = null;
Boolean flagged = null;
Boolean snoozed = null;
Boolean encrypted = null;
Boolean attachments = null;
String find = (TextUtils.isEmpty(query) ? null : query.toLowerCase());
if (find != null && find.startsWith(context.getString(R.string.title_search_special_prefix) + ":")) {
String special = find.split(":")[1];
if (context.getString(R.string.title_search_special_unseen).equals(special))
seen = false;
else if (context.getString(R.string.title_search_special_flagged).equals(special))
flagged = true;
else if (context.getString(R.string.title_search_special_snoozed).equals(special))
snoozed = true;
else if (context.getString(R.string.title_search_special_encrypted).equals(special))
encrypted = true;
else if (context.getString(R.string.title_search_special_attachments).equals(special))
attachments = true;
}
int found = 0; int found = 0;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean fts = prefs.getBoolean("fts", false); boolean fts = prefs.getBoolean("fts", false);
boolean pro = ActivityBilling.isPro(context); boolean pro = ActivityBilling.isPro(context);
if (fts && pro && seen == null && flagged == null && snoozed == null && encrypted == null && attachments == null) { if (fts && pro && criteria.isQueryOnly()) {
if (state.ids == null) { if (state.ids == null) {
SQLiteDatabase sdb = FtsDbHelper.getInstance(context); SQLiteDatabase sdb = FtsDbHelper.getInstance(context);
state.ids = FtsDbHelper.match(sdb, account, folder, query); state.ids = FtsDbHelper.match(sdb, account, folder, criteria.query);
} }
try { try {
@ -237,16 +221,15 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
(state.matches.size() > 0 && state.index >= state.matches.size())) { (state.matches.size() > 0 && state.index >= state.matches.size())) {
state.matches = db.message().matchMessages( state.matches = db.message().matchMessages(
account, folder, account, folder,
"%" + find + "%", criteria.query == null ? null : "%" + criteria.query + "%",
seen, flagged, snoozed, encrypted, attachments, criteria.with_unseen,
criteria.with_flagged,
criteria.with_hidden,
criteria.with_encrypted,
criteria.with_attachments,
SEARCH_LIMIT, state.offset); SEARCH_LIMIT, state.offset);
Log.i("Boundary device folder=" + folder + Log.i("Boundary device folder=" + folder +
" query=" + query + " criteria=" + criteria +
" seen=" + seen +
" flagged=" + flagged +
" snoozed=" + snoozed +
" encrypted=" + encrypted +
" attachments=" + attachments +
" offset=" + state.offset + " offset=" + state.offset +
" size=" + state.matches.size()); " size=" + state.matches.size());
state.offset += Math.min(state.matches.size(), SEARCH_LIMIT); state.offset += Math.min(state.matches.size(), SEARCH_LIMIT);
@ -260,25 +243,20 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
state.index = i + 1; state.index = i + 1;
TupleMatch match = state.matches.get(i); TupleMatch match = state.matches.get(i);
if (criteria.query != null && (match.matched == null || !match.matched))
if (find == null || seen != null || flagged != null || snoozed != null || encrypted != null || attachments != null) try {
match.matched = true; File file = EntityMessage.getFile(context, match.id);
else { if (file.exists()) {
if (match.matched == null || !match.matched) String html = Helper.readText(file);
try { if (html.toLowerCase().contains(criteria.query)) {
File file = EntityMessage.getFile(context, match.id); String text = HtmlHelper.getFullText(html);
if (file.exists()) { if (text.toLowerCase().contains(criteria.query))
String html = Helper.readText(file); match.matched = true;
if (html.toLowerCase().contains(find)) {
String text = HtmlHelper.getFullText(html);
if (text.toLowerCase().contains(find))
match.matched = true;
}
} }
} catch (IOException ex) {
Log.e(ex);
} }
} } catch (IOException ex) {
Log.e(ex);
}
if (match.matched != null && match.matched) { if (match.matched != null && match.matched) {
found++; found++;
@ -303,10 +281,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
DB db = DB.getInstance(context); DB db = DB.getInstance(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean search_text = prefs.getBoolean("search_text", false);
final boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE); final boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE);
final EntityFolder browsable = db.folder().getBrowsableFolder(folder, query != null); final EntityFolder browsable = db.folder().getBrowsableFolder(folder, criteria != null);
if (browsable == null || !browsable.selectable) { if (browsable == null || !browsable.selectable) {
Log.w("Boundary not browsable=" + (folder != null)); Log.w("Boundary not browsable=" + (folder != null));
return 0; return 0;
@ -344,8 +321,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
int count = state.ifolder.getMessageCount(); int count = state.ifolder.getMessageCount();
db.folder().setFolderTotal(browsable.id, count < 0 ? null : count); db.folder().setFolderTotal(browsable.id, count < 0 ? null : count);
Log.i("Boundary server query=" + query); Log.i("Boundary server query=" + criteria.query);
if (query == null) { if (criteria == null) {
boolean filter_seen = prefs.getBoolean("filter_seen", false); boolean filter_seen = prefs.getBoolean("filter_seen", false);
boolean filter_unflagged = prefs.getBoolean("filter_unflagged", false); boolean filter_unflagged = prefs.getBoolean("filter_unflagged", false);
Log.i("Boundary filter seen=" + filter_seen + " unflagged=" + filter_unflagged); Log.i("Boundary filter seen=" + filter_seen + " unflagged=" + filter_unflagged);
@ -366,67 +343,28 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
state.imessages = state.ifolder.search(searchFlagged); state.imessages = state.ifolder.search(searchFlagged);
else else
state.imessages = state.ifolder.getMessages(); state.imessages = state.ifolder.getMessages();
} else if (query.startsWith(context.getString(R.string.title_search_special_prefix) + ":")) {
String special = query.split(":")[1];
if (context.getString(R.string.title_search_special_unseen).equals(special))
state.imessages = state.ifolder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
else if (context.getString(R.string.title_search_special_flagged).equals(special))
state.imessages = state.ifolder.search(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
else
state.imessages = new Message[0];
} else { } else {
Object result = state.ifolder.doCommand(new IMAPFolder.ProtocolCommand() { Object result = state.ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override @Override
public Object doCommand(IMAPProtocol protocol) { public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
// Yahoo! does not support keyword search, but uses the flags $Forwarded $Junk $NotJunk
boolean keywords = false;
for (String keyword : browsable.keywords)
if (!keyword.startsWith("$")) {
keywords = true;
break;
}
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 (query.startsWith("raw:") && state.iservice.hasCapability("X-GM-EXT-1")) { if (criteria.query != null &&
criteria.query.startsWith("raw:") &&
state.iservice.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(query.substring(4)); arg.writeString(criteria.query.substring(4));
} else {
if (!protocol.supportsUtf8()) { Response[] responses = protocol.command("SEARCH", arg);
arg.writeAtom("CHARSET"); if (responses.length == 0)
arg.writeAtom(StandardCharsets.UTF_8.name()); throw new ProtocolException("No response");
} if (!responses[responses.length - 1].isOK())
arg.writeAtom("OR"); throw new ProtocolException(responses[responses.length - 1]);
arg.writeAtom("OR");
arg.writeAtom("OR");
if (search_text)
arg.writeAtom("OR");
if (keywords)
arg.writeAtom("OR");
arg.writeAtom("FROM");
arg.writeBytes(query.getBytes());
arg.writeAtom("TO");
arg.writeBytes(query.getBytes());
arg.writeAtom("CC");
arg.writeBytes(query.getBytes());
arg.writeAtom("SUBJECT");
arg.writeBytes(query.getBytes());
if (search_text) {
arg.writeAtom("BODY");
arg.writeBytes(query.getBytes());
}
if (keywords) {
arg.writeAtom("KEYWORD");
arg.writeBytes(query.getBytes());
}
}
Response[] responses = protocol.command("SEARCH", arg); Log.i("Boundary raw search=" + criteria.query);
if (responses.length > 0 && responses[responses.length - 1].isOK()) {
Log.i("Boundary UTF8 search=" + query);
List<Integer> msgnums = new ArrayList<>(); List<Integer> msgnums = new ArrayList<>();
for (Response response : responses) for (Response response : responses)
@ -442,37 +380,68 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return imessages; return imessages;
} else { } else {
if (responses.length > 0) Log.i("Boundary search=" + criteria);
Log.e("Search response=" + responses[responses.length - 1]);
List<SearchTerm> or = new ArrayList<>();
// Assume no UTF-8 support List<SearchTerm> and = new ArrayList<>();
String search = query.replace("ß", "ss"); // Eszett if (criteria.query != null) {
search = Normalizer.normalize(search, Normalizer.Form.NFD) String search = criteria.query;
.replaceAll("[^\\p{ASCII}]", "");
if (!protocol.supportsUtf8()) {
Log.i("Boundary ASCII search=" + search); search = search.replace("ß", "ss"); // Eszett
SearchTerm term = new FromStringTerm(search); search = Normalizer.normalize(search, Normalizer.Form.NFD)
term = new OrTerm(term, new RecipientStringTerm(Message.RecipientType.TO, search)); .replaceAll("[^\\p{ASCII}]", "");
term = new OrTerm(term, new RecipientStringTerm(Message.RecipientType.CC, search)); }
term = new OrTerm(term, new SubjectTerm(search));
if (search_text) // Yahoo! does not support keyword search, but uses the flags $Forwarded $Junk $NotJunk
term = new OrTerm(term, new BodyTerm(search)); boolean keywords = false;
if (keywords) for (String keyword : browsable.keywords)
term = new OrTerm(term, new FlagTerm( if (!keyword.startsWith("$")) {
new Flags(MessageHelper.sanitizeKeyword(search)), true)); keywords = true;
break;
}
if (criteria.in_senders)
or.add(new FromStringTerm(search));
if (criteria.in_receipients) {
or.add(new RecipientStringTerm(Message.RecipientType.TO, search));
or.add(new RecipientStringTerm(Message.RecipientType.CC, search));
}
if (criteria.in_subject)
or.add(new SubjectTerm(search));
if (criteria.in_keywords && keywords)
or.add(new FlagTerm(new Flags(MessageHelper.sanitizeKeyword(search)), true));
if (criteria.in_message)
or.add(new BodyTerm(search));
}
if (criteria.with_unseen)
and.add(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
if (criteria.with_flagged)
and.add(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
SearchTerm term = null;
if (or.size() > 0)
term = new OrTerm(or.toArray(new SearchTerm[0]));
if (and.size() > 0)
if (term == null)
term = new AndTerm(and.toArray(new SearchTerm[0]));
else
term = new AndTerm(term, new AndTerm(and.toArray(new SearchTerm[0])));
if (term == null)
throw new SearchException();
return state.ifolder.search(term); return state.ifolder.search(term);
} }
} catch (MessagingException ex) { } catch (MessagingException ex) {
Log.e(ex); throw new ProtocolException("Search", ex);
return ex;
} }
} }
}); });
if (result instanceof MessagingException)
throw (MessagingException) result;
state.imessages = (Message[]) result; state.imessages = (Message[]) result;
} }
Log.i("Boundary server found messages=" + state.imessages.length); Log.i("Boundary server found messages=" + state.imessages.length);
@ -541,7 +510,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
rules, astate); rules, astate);
found++; found++;
} }
if (message != null && query != null /* browsed */) if (message != null && criteria != null /* browsed */)
db.message().setMessageFound(message.id); db.message().setMessageFound(message.id);
} catch (MessageRemovedException ex) { } catch (MessageRemovedException ex) {
Log.w(browsable.name + " boundary server", ex); Log.w(browsable.name + " boundary server", ex);
@ -623,4 +592,87 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
imessages = null; imessages = null;
} }
} }
static class SearchCriteria implements Serializable {
String query;
boolean in_senders = true;
boolean in_receipients = true;
boolean in_subject = true;
boolean in_keywords = true;
boolean in_message = true;
boolean with_unseen;
boolean with_flagged;
boolean with_hidden;
boolean with_encrypted;
boolean with_attachments;
SearchCriteria() {
}
SearchCriteria(String query) {
this.query = query;
}
boolean isQueryOnly() {
return (!TextUtils.isEmpty(query) &&
in_senders &&
in_receipients &&
in_subject &&
in_keywords &&
in_message &&
isWithout());
}
boolean isWithout() {
return !(with_unseen ||
with_flagged ||
with_hidden ||
with_encrypted ||
with_attachments);
}
String getTitle() {
return query +
(with_unseen ? " ?" : "") +
(with_flagged ? " ★" : "") +
(with_hidden ? " ∅" : "") +
(with_encrypted ? " ✗" : "") +
(with_attachments ? " &" : "");
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof SearchCriteria) {
SearchCriteria other = (SearchCriteria) obj;
return (Objects.equals(this.query, other.query) &&
this.in_senders == other.in_senders &&
this.in_receipients == other.in_receipients &&
this.in_subject == other.in_subject &&
this.in_keywords == other.in_keywords &&
this.in_message == other.in_message &&
this.with_unseen == other.with_unseen &&
this.with_flagged == other.with_flagged &&
this.with_hidden == other.with_hidden &&
this.with_encrypted == other.with_encrypted &&
this.with_attachments == other.with_attachments);
} else
return false;
}
@NonNull
@Override
public String toString() {
return query +
" senders=" + in_senders +
" receipients=" + in_receipients +
" subject=" + in_subject +
" keywords=" + in_keywords +
" message=" + in_message +
" unseen=" + with_unseen +
" flagged=" + with_flagged +
" hidden=" + with_hidden +
" encrypted=" + with_encrypted +
" attachments=" + with_attachments;
}
}
} }

@ -290,16 +290,16 @@ public interface DaoMessage {
" WHERE NOT ui_hide" + " WHERE NOT ui_hide" +
" AND (:account IS NULL OR account = :account)" + " AND (:account IS NULL OR account = :account)" +
" AND (:folder IS NULL OR folder = :folder)" + " AND (:folder IS NULL OR folder = :folder)" +
" AND (:seen IS NULL OR ui_seen = :seen)" + " AND (NOT :unseen OR NOT ui_seen)" +
" AND (:flagged IS NULL OR ui_flagged = :flagged)" + " AND (NOT :flagged OR ui_flagged)" +
" AND (:hidden IS NULL OR (CASE WHEN ui_snoozed IS NULL THEN 0 ELSE 1 END) = :hidden)" + " AND (NOT :hidden OR NOT ui_snoozed IS NULL)" +
" AND (:encrypted IS NULL OR ui_encrypt > 0)" + " AND (NOT :encrypted OR ui_encrypt > 0)" +
" AND (:attachments IS NULL OR attachments > 0)" + " AND (NOT :attachments OR attachments > 0)" +
" ORDER BY received DESC" + " ORDER BY received DESC" +
" LIMIT :limit OFFSET :offset") " LIMIT :limit OFFSET :offset")
List<TupleMatch> matchMessages( List<TupleMatch> matchMessages(
Long account, Long folder, String find, Long account, Long folder, String find,
Boolean seen, Boolean flagged, Boolean hidden, Boolean encrypted, Boolean attachments, boolean unseen, boolean flagged, boolean hidden, boolean encrypted, boolean attachments,
int limit, int offset); int limit, int offset);
@Query("SELECT id" + @Query("SELECT id" +

@ -71,7 +71,6 @@ public class FragmentAccounts extends FragmentBase {
private FloatingActionButton fabCompose; private FloatingActionButton fabCompose;
private ObjectAnimator animator; private ObjectAnimator animator;
private String searching = null;
private AdapterAccount adapter; private AdapterAccount adapter;
private static final int REQUEST_IMPORT_OAUTH = 1; private static final int REQUEST_IMPORT_OAUTH = 1;
@ -247,19 +246,10 @@ public class FragmentAccounts extends FragmentBase {
return view; return view;
} }
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("fair:searching", searching);
super.onSaveInstanceState(outState);
}
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
searching = savedInstanceState.getString("fair:searching");
DB db = DB.getInstance(getContext()); DB db = DB.getInstance(getContext());
// Observe accounts // Observe accounts
@ -294,23 +284,6 @@ public class FragmentAccounts extends FragmentBase {
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_accounts, menu); inflater.inflate(R.menu.menu_accounts, menu);
MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchViewEx searchView = (SearchViewEx) menuSearch.getActionView();
searchView.setup(getViewLifecycleOwner(), menuSearch, searching, new SearchViewEx.ISearch() {
@Override
public void onSave(String query) {
searching = query;
}
@Override
public void onSearch(String query) {
FragmentMessages.search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
-1, -1, false, query);
}
});
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
} }
@ -324,6 +297,9 @@ public class FragmentAccounts extends FragmentBase {
@Override @Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_search:
onMenuSearch();
return true;
case R.id.menu_force_sync: case R.id.menu_force_sync:
onMenuForceSync(); onMenuForceSync();
return true; return true;
@ -332,6 +308,14 @@ public class FragmentAccounts extends FragmentBase {
} }
} }
private void onMenuSearch() {
Bundle args = new Bundle();
FragmentDialogSearch fragment = new FragmentDialogSearch();
fragment.setArguments(args);
fragment.show(getParentFragmentManager(), "search");
}
private void onMenuForceSync() { private void onMenuForceSync() {
ServiceSynchronize.reload(getContext(), null, true, "force sync"); ServiceSynchronize.reload(getContext(), null, true, "force sync");
ToastEx.makeText(getContext(), R.string.title_executing, Toast.LENGTH_LONG).show(); ToastEx.makeText(getContext(), R.string.title_executing, Toast.LENGTH_LONG).show();

@ -0,0 +1,171 @@
package eu.faircode.email;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AutoCompleteTextView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.FilterQueryProvider;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.cursoradapter.widget.SimpleCursorAdapter;
import androidx.preference.PreferenceManager;
public class FragmentDialogSearch extends FragmentDialogBase {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_search, null);
final AutoCompleteTextView etQuery = dview.findViewById(R.id.etQuery);
final CheckBox cbSearchIndex = dview.findViewById(R.id.cbSearchIndex);
final CheckBox cbSenders = dview.findViewById(R.id.cbSenders);
final CheckBox cbRecipients = dview.findViewById(R.id.cbRecipients);
final CheckBox cbSubject = dview.findViewById(R.id.cbSubject);
final CheckBox cbKeywords = dview.findViewById(R.id.cbKeywords);
final CheckBox cbMessage = dview.findViewById(R.id.cbMessage);
final CheckBox cbUnseen = dview.findViewById(R.id.cbUnseen);
final CheckBox cbFlagged = dview.findViewById(R.id.cbFlagged);
final CheckBox cbHidden = dview.findViewById(R.id.cbHidden);
final CheckBox cbEncrypted = dview.findViewById(R.id.cbEncrypted);
final CheckBox cbAttachments = dview.findViewById(R.id.cbAttachments);
boolean pro = ActivityBilling.isPro(getContext());
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean fts = prefs.getBoolean("fts", false);
boolean filter_seen = prefs.getBoolean("filter_seen", false);
boolean filter_unflagged = prefs.getBoolean("filter_unflagged", false);
String last_search = prefs.getString("last_search", null);
if (!TextUtils.isEmpty(last_search)) {
etQuery.setText(last_search);
etQuery.setSelection(0, last_search.length());
}
etQuery.requestFocus();
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
getContext(),
R.layout.search_suggestion,
null,
new String[]{"suggestion"},
new int[]{android.R.id.text1},
0);
adapter.setFilterQueryProvider(new FilterQueryProvider() {
public Cursor runQuery(CharSequence typed) {
Log.i("Search suggest=" + typed);
MatrixCursor cursor = new MatrixCursor(new String[]{"_id", "suggestion"});
if (TextUtils.isEmpty(typed))
return cursor;
String query = "%" + typed + "%";
DB db = DB.getInstance(getContext());
return db.message().getSuggestions("%" + query + "%");
}
});
etQuery.setAdapter(adapter);
cbSearchIndex.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
cbSenders.setEnabled(!isChecked);
cbRecipients.setEnabled(!isChecked);
cbSubject.setEnabled(!isChecked);
cbKeywords.setEnabled(!isChecked);
cbMessage.setEnabled(!isChecked);
cbUnseen.setEnabled(!isChecked);
cbFlagged.setEnabled(!isChecked);
cbHidden.setEnabled(!isChecked);
cbEncrypted.setEnabled(!isChecked);
cbAttachments.setEnabled(!isChecked);
}
});
cbSearchIndex.setChecked(fts && pro);
cbSearchIndex.setEnabled(pro);
cbUnseen.setChecked(filter_seen);
cbFlagged.setChecked(filter_unflagged);
final AlertDialog dialog = new AlertDialog.Builder(getContext())
.setView(dview)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
long account = getArguments().getLong("account", -1);
long folder = getArguments().getLong("folder", -1);
BoundaryCallbackMessages.SearchCriteria criteria = new BoundaryCallbackMessages.SearchCriteria();
criteria.query = etQuery.getText().toString();
if (TextUtils.isEmpty(criteria.query))
criteria.query = null;
else
prefs.edit().putString("last_search", criteria.query).apply();
if (!cbSearchIndex.isChecked()) {
criteria.in_senders = cbSenders.isChecked();
criteria.in_receipients = cbRecipients.isChecked();
criteria.in_subject = cbSubject.isChecked();
criteria.in_keywords = cbKeywords.isChecked();
criteria.in_message = cbMessage.isChecked();
criteria.with_unseen = cbUnseen.isChecked();
criteria.with_flagged = cbFlagged.isChecked();
criteria.with_hidden = cbHidden.isChecked();
criteria.with_encrypted = cbEncrypted.isChecked();
criteria.with_attachments = cbAttachments.isChecked();
}
FragmentMessages.search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
account, folder, false, criteria);
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Do nothing
}
})
.setNeutralButton(R.string.title_info, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Helper.viewFAQ(getContext(), 13);
}
})
.create();
etQuery.setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
return true;
}
return false;
}
});
return dialog;
}
}

@ -83,7 +83,6 @@ public class FragmentFolders extends FragmentBase {
private long account; private long account;
private boolean primary; private boolean primary;
private boolean show_hidden = false; private boolean show_hidden = false;
private String searching = null;
private AdapterFolder adapter; private AdapterFolder adapter;
private NumberFormat NF = NumberFormat.getNumberInstance(); private NumberFormat NF = NumberFormat.getNumberInstance();
@ -263,20 +262,10 @@ public class FragmentFolders extends FragmentBase {
return view; return view;
} }
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("fair:searching", searching);
super.onSaveInstanceState(outState);
}
@Override @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
searching = savedInstanceState.getString("fair:searching");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
grpHintActions.setVisibility(prefs.getBoolean("folder_actions", false) ? View.GONE : View.VISIBLE); grpHintActions.setVisibility(prefs.getBoolean("folder_actions", false) ? View.GONE : View.VISIBLE);
grpHintSync.setVisibility(prefs.getBoolean("folder_sync", false) ? View.GONE : View.VISIBLE); grpHintSync.setVisibility(prefs.getBoolean("folder_sync", false) ? View.GONE : View.VISIBLE);
@ -428,23 +417,6 @@ public class FragmentFolders extends FragmentBase {
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_folders, menu); inflater.inflate(R.menu.menu_folders, menu);
MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchViewEx searchView = (SearchViewEx) menuSearch.getActionView();
searchView.setup(getViewLifecycleOwner(), menuSearch, searching, new SearchViewEx.ISearch() {
@Override
public void onSave(String query) {
searching = query;
}
@Override
public void onSearch(String query) {
FragmentMessages.search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
account, -1, false, query);
}
});
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
} }
@ -466,6 +438,9 @@ public class FragmentFolders extends FragmentBase {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_search:
onMenuSearch();
return true;
case R.id.menu_compact: case R.id.menu_compact:
onMenuCompact(); onMenuCompact();
return true; return true;
@ -486,6 +461,15 @@ public class FragmentFolders extends FragmentBase {
} }
} }
private void onMenuSearch() {
Bundle args = new Bundle();
args.putLong("account", account);
FragmentDialogSearch fragment = new FragmentDialogSearch();
fragment.setArguments(args);
fragment.show(getParentFragmentManager(), "search");
}
private void onMenuCompact() { private void onMenuCompact() {
compact = !compact; compact = !compact;

@ -247,7 +247,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
private long id; private long id;
private boolean filter_archive; private boolean filter_archive;
private boolean found; private boolean found;
private String query; private BoundaryCallbackMessages.SearchCriteria criteria = null;
private boolean pane; private boolean pane;
private long message = -1; private long message = -1;
@ -272,7 +272,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
private long primary; private long primary;
private boolean connected; private boolean connected;
private boolean reset = false; private boolean reset = false;
private String searching = null;
private boolean initialized = false; private boolean initialized = false;
private boolean loading = false; private boolean loading = false;
private boolean swiping = false; private boolean swiping = false;
@ -356,12 +355,12 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
id = args.getLong("id", -1); id = args.getLong("id", -1);
filter_archive = args.getBoolean("filter_archive", true); filter_archive = args.getBoolean("filter_archive", true);
found = args.getBoolean("found", false); found = args.getBoolean("found", false);
query = args.getString("query"); criteria = (BoundaryCallbackMessages.SearchCriteria) args.getSerializable("criteria");
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 (folder > 0 && type == null && TextUtils.isEmpty(query)) if (folder > 0 && type == null && criteria == null)
Log.e("Messages for folder without type"); Log.e("Messages for folder without type");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
@ -382,7 +381,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
colorPrimary = Helper.resolveColor(getContext(), R.attr.colorPrimary); colorPrimary = Helper.resolveColor(getContext(), R.attr.colorPrimary);
colorAccent = Helper.resolveColor(getContext(), R.attr.colorAccent); colorAccent = Helper.resolveColor(getContext(), R.attr.colorAccent);
if (TextUtils.isEmpty(query)) if (criteria == null)
if (thread == null) { if (thread == null) {
if (folder < 0) if (folder < 0)
viewType = AdapterMessage.ViewType.UNIFIED; viewType = AdapterMessage.ViewType.UNIFIED;
@ -1019,40 +1018,23 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
ss.setSpan(new RelativeSizeSpan(0.9f), 0, ss.length(), 0); ss.setSpan(new RelativeSizeSpan(0.9f), 0, ss.length(), 0);
popupMenu.getMenu().add(Menu.NONE, 0, order++, ss) popupMenu.getMenu().add(Menu.NONE, 0, order++, ss)
.setEnabled(false); .setEnabled(false);
popupMenu.getMenu().add(Menu.NONE, 1, order++, R.string.title_search_text)
.setCheckable(true).setChecked(search_text);
String folderName = args.getString("folderName", null); String folderName = args.getString("folderName", null);
if (!TextUtils.isEmpty(folderName)) if (!TextUtils.isEmpty(folderName))
popupMenu.getMenu().add(Menu.NONE, 2, order++, folderName); popupMenu.getMenu().add(Menu.NONE, 1, order++, folderName);
for (EntityAccount account : accounts) for (EntityAccount account : accounts)
popupMenu.getMenu().add(Menu.NONE, 3, order++, account.name) popupMenu.getMenu().add(Menu.NONE, 2, order++, account.name)
.setIntent(new Intent().putExtra("account", account.id)); .setIntent(new Intent().putExtra("account", account.id));
popupMenu.getMenu().add(Menu.NONE, 999, order++, R.string.title_setup_help);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override @Override
public boolean onMenuItemClick(MenuItem target) { public boolean onMenuItemClick(MenuItem target) {
switch (target.getItemId()) { if (target.getItemId() == 1) { // Search same folder
case 1: // Search text search(
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
boolean search_text = prefs.getBoolean("search_text", false); account, folder, true, criteria);
prefs.edit().putBoolean("search_text", !search_text).apply(); return true;
return true;
case 2: // Search same folder
search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
account, folder,
true,
query);
return true;
case 999: // Help
Helper.viewFAQ(getContext(), 13);
return true;
} }
Intent intent = target.getIntent(); Intent intent = target.getIntent();
@ -1063,7 +1045,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
args.putString("title", getString(R.string.title_search_in)); args.putString("title", getString(R.string.title_search_in));
args.putLong("account", intent.getLongExtra("account", -1)); args.putLong("account", intent.getLongExtra("account", -1));
args.putLongArray("disabled", new long[]{}); args.putLongArray("disabled", new long[]{});
args.putString("query", query); args.putSerializable("criteria", criteria);
FragmentDialogFolder fragment = new FragmentDialogFolder(); FragmentDialogFolder fragment = new FragmentDialogFolder();
fragment.setArguments(args); fragment.setArguments(args);
@ -1122,16 +1104,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
else else
fabCompose.hide(); fabCompose.hide();
if (viewType == AdapterMessage.ViewType.SEARCH && !server) { if (viewType == AdapterMessage.ViewType.SEARCH && criteria != null && !server) {
if (query != null && query.startsWith(getString(R.string.title_search_special_prefix) + ":")) { if (criteria.with_hidden || criteria.with_encrypted || criteria.with_attachments)
String special = query.split(":")[1]; fabSearch.hide();
if (getString(R.string.title_search_special_snoozed).equals(special) || else
getString(R.string.title_search_special_encrypted).equals(special) ||
getString(R.string.title_search_special_attachments).equals(special))
fabSearch.hide();
else
fabSearch.show();
} else
fabSearch.show(); fabSearch.show();
} else } else
fabSearch.hide(); fabSearch.hide();
@ -2949,8 +2925,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
outState.putBoolean("fair:reset", reset); outState.putBoolean("fair:reset", reset);
outState.putString("fair:searching", searching);
outState.putBoolean("fair:autoExpanded", autoExpanded); outState.putBoolean("fair:autoExpanded", autoExpanded);
outState.putInt("fair:autoCloseCount", autoCloseCount); outState.putInt("fair:autoCloseCount", autoCloseCount);
@ -2975,8 +2949,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
if (savedInstanceState != null) { if (savedInstanceState != null) {
reset = savedInstanceState.getBoolean("fair:reset"); reset = savedInstanceState.getBoolean("fair:reset");
searching = savedInstanceState.getString("fair:searching");
autoExpanded = savedInstanceState.getBoolean("fair:autoExpanded"); autoExpanded = savedInstanceState.getBoolean("fair:autoExpanded");
autoCloseCount = savedInstanceState.getInt("fair:autoCloseCount"); autoCloseCount = savedInstanceState.getInt("fair:autoCloseCount");
@ -3108,7 +3080,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
break; break;
case SEARCH: case SEARCH:
setSubtitle(query); setSubtitle(criteria.getTitle());
break; break;
} }
@ -3347,22 +3319,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_messages, menu); inflater.inflate(R.menu.menu_messages, menu);
MenuItem menuSearch = menu.findItem(R.id.menu_search);
SearchViewEx searchView = (SearchViewEx) menuSearch.getActionView();
searchView.setup(getViewLifecycleOwner(), menuSearch, searching, new SearchViewEx.ISearch() {
@Override
public void onSave(String query) {
searching = query;
}
@Override
public void onSearch(String query) {
FragmentMessages.search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
account, folder, false, query);
}
});
menu.findItem(R.id.menu_folders).setActionView(R.layout.action_button); menu.findItem(R.id.menu_folders).setActionView(R.layout.action_button);
ImageButton ib = (ImageButton) menu.findItem(R.id.menu_folders).getActionView(); ImageButton ib = (ImageButton) menu.findItem(R.id.menu_folders).getActionView();
ib.setImageResource(R.drawable.baseline_folder_24); ib.setImageResource(R.drawable.baseline_folder_24);
@ -3414,8 +3370,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
menuSearch.setVisible( menuSearch.setVisible(
(viewType == AdapterMessage.ViewType.UNIFIED && type == null) (viewType == AdapterMessage.ViewType.UNIFIED && type == null)
|| viewType == AdapterMessage.ViewType.FOLDER); || viewType == AdapterMessage.ViewType.FOLDER);
if (!menuSearch.isVisible())
menuSearch.collapseActionView();
menu.findItem(R.id.menu_folders).setVisible(viewType == AdapterMessage.ViewType.UNIFIED && primary >= 0); menu.findItem(R.id.menu_folders).setVisible(viewType == AdapterMessage.ViewType.UNIFIED && primary >= 0);
ImageButton ib = (ImageButton) menu.findItem(R.id.menu_folders).getActionView(); ImageButton ib = (ImageButton) menu.findItem(R.id.menu_folders).getActionView();
@ -3494,6 +3448,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_search:
onMenuSearch();
return true;
case R.id.menu_folders: case R.id.menu_folders:
// Obsolete // Obsolete
onMenuFolders(primary); onMenuFolders(primary);
@ -3602,6 +3560,16 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
} }
} }
private void onMenuSearch() {
Bundle args = new Bundle();
args.putLong("account", account);
args.putLong("folder", folder);
FragmentDialogSearch fragment = new FragmentDialogSearch();
fragment.setArguments(args);
fragment.show(getParentFragmentManager(), "search");
}
private void onMenuFolders(long account) { private void onMenuFolders(long account) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
getParentFragmentManager().popBackStack("unified", 0); getParentFragmentManager().popBackStack("unified", 0);
@ -3887,7 +3855,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
ViewModelMessages.Model vmodel = model.getModel( ViewModelMessages.Model vmodel = model.getModel(
getContext(), getViewLifecycleOwner(), getContext(), getViewLifecycleOwner(),
viewType, type, account, folder, thread, id, filter_archive, query, server); viewType, type, account, folder, thread, id, filter_archive, criteria, server);
vmodel.setCallback(getViewLifecycleOwner(), callback); vmodel.setCallback(getViewLifecycleOwner(), callback);
vmodel.setObserver(getViewLifecycleOwner(), observer); vmodel.setObserver(getViewLifecycleOwner(), observer);
@ -4969,12 +4937,12 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
case REQUEST_SEARCH: case REQUEST_SEARCH:
if (resultCode == RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
Bundle args = data.getBundleExtra("args"); Bundle args = data.getBundleExtra("args");
search( BoundaryCallbackMessages.SearchCriteria criteria =
getContext(), getViewLifecycleOwner(), getParentFragmentManager(), (BoundaryCallbackMessages.SearchCriteria) args.getSerializable("criteria");
search(getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
args.getLong("account"), args.getLong("account"),
args.getLong("folder"), args.getLong("folder"),
true, true, criteria);
args.getString("query"));
} }
break; break;
case REQUEST_ACCOUNT: case REQUEST_ACCOUNT:
@ -6506,6 +6474,14 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
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 account, long folder, boolean server, String query) { long account, long folder, boolean server, String query) {
search(context, owner, manager,
account, folder,
server, new BoundaryCallbackMessages.SearchCriteria(query));
}
static void search(
final Context context, final LifecycleOwner owner, final FragmentManager manager,
long account, long folder, boolean server, BoundaryCallbackMessages.SearchCriteria criteria) {
if (server && !ActivityBilling.isPro(context)) { if (server && !ActivityBilling.isPro(context)) {
context.startActivity(new Intent(context, ActivityBilling.class)); context.startActivity(new Intent(context, ActivityBilling.class));
return; return;
@ -6518,7 +6494,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
args.putLong("account", account); args.putLong("account", account);
args.putLong("folder", folder); args.putLong("folder", folder);
args.putBoolean("server", server); args.putBoolean("server", server);
args.putString("query", query); args.putSerializable("criteria", criteria);
FragmentMessages fragment = new FragmentMessages(); FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(args); fragment.setArguments(args);

@ -1,195 +0,0 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2020 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MenuItem;
import android.widget.AutoCompleteTextView;
import android.widget.Toast;
import androidx.appcompat.widget.SearchView;
import androidx.cursoradapter.widget.SimpleCursorAdapter;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager;
public class SearchViewEx extends SearchView {
private String _searching = null;
private boolean expanding = false;
private boolean collapsing = false;
public SearchViewEx(Context context) {
super(context);
init(context);
}
public SearchViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SearchViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setQueryHint(context.getString(R.string.title_search));
AutoCompleteTextView autoCompleteTextView = findViewById(androidx.appcompat.R.id.search_src_text);
autoCompleteTextView.setThreshold(0);
}
@Override
public void onActionViewExpanded() {
expanding = true;
super.onActionViewExpanded();
expanding = false;
}
@Override
public void onActionViewCollapsed() {
collapsing = true;
super.onActionViewCollapsed();
collapsing = false;
}
void setup(LifecycleOwner owner, MenuItem menuSearch, String searching, ISearch intf) {
_searching = searching;
if (!TextUtils.isEmpty(_searching))
post(new Runnable() {
@Override
public void run() {
//menuSearch.expandActionView();
setQuery(_searching, false);
}
});
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextChange(String newText) {
if (!expanding && !collapsing) {
_searching = newText;
intf.onSave(_searching);
}
if (TextUtils.isEmpty(_searching)) {
MatrixCursor cursor = new MatrixCursor(new String[]{"_id", "suggestion"});
String last_search = prefs.getString("last_search", null);
if (!TextUtils.isEmpty(last_search))
cursor.addRow(new Object[]{-1, last_search});
String prefix = getContext().getString(R.string.title_search_special_prefix);
cursor.addRow(new Object[]{-2, prefix + ":" + getContext().getString(R.string.title_search_special_unseen)});
cursor.addRow(new Object[]{-3, prefix + ":" + getContext().getString(R.string.title_search_special_flagged)});
cursor.addRow(new Object[]{-4, prefix + ":" + getContext().getString(R.string.title_search_special_snoozed)});
cursor.addRow(new Object[]{-5, prefix + ":" + getContext().getString(R.string.title_search_special_encrypted)});
cursor.addRow(new Object[]{-6, prefix + ":" + getContext().getString(R.string.title_search_special_attachments)});
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
getContext(),
R.layout.search_suggestion,
cursor,
new String[]{"suggestion"},
new int[]{android.R.id.text1},
0);
setSuggestionsAdapter(adapter);
adapter.notifyDataSetChanged();
} else {
Bundle args = new Bundle();
args.putString("query", _searching);
new SimpleTask<Cursor>() {
@Override
protected Cursor onExecute(Context context, Bundle args) {
String query = args.getString("query");
DB db = DB.getInstance(context);
return db.message().getSuggestions("%" + query + "%");
}
@Override
protected void onExecuted(Bundle args, Cursor cursor) {
Log.i("Suggestions=" + cursor.getCount());
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
getContext(),
R.layout.search_suggestion,
cursor,
new String[]{"suggestion"},
new int[]{android.R.id.text1},
0);
setSuggestionsAdapter(adapter);
adapter.notifyDataSetChanged();
}
@Override
protected void onException(Bundle args, Throwable ex) {
ToastEx.makeText(getContext(), Log.formatThrowable(ex), Toast.LENGTH_LONG).show();
}
}.execute(getContext(), owner, args, "messages:suggestions");
}
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
_searching = null;
intf.onSave(query);
menuSearch.collapseActionView();
intf.onSearch(query);
String prefix = getContext().getString(R.string.title_search_special_prefix);
if (query != null && !query.startsWith(prefix + ":"))
prefs.edit().putString("last_search", query).apply();
return true;
}
});
setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) {
return false;
}
@Override
public boolean onSuggestionClick(int position) {
Cursor cursor = (Cursor) getSuggestionsAdapter().getItem(position);
long id = cursor.getInt(0);
setQuery(cursor.getString(1), id != -1);
return (id == -1);
}
});
}
interface ISearch {
void onSave(String query);
void onSearch(String query);
}
}

@ -62,9 +62,9 @@ public class ViewModelMessages extends ViewModel {
final AdapterMessage.ViewType viewType, final AdapterMessage.ViewType viewType,
String type, long account, long folder, String type, long account, long folder,
String thread, long id, boolean filter_archive, String thread, long id, boolean filter_archive,
String query, boolean server) { BoundaryCallbackMessages.SearchCriteria criteria, boolean server) {
Args args = new Args(context, viewType, type, account, folder, thread, id, filter_archive, query, server); Args args = new Args(context, viewType, type, account, folder, thread, id, filter_archive, criteria, server);
Log.d("Get model=" + viewType + " " + args); Log.d("Get model=" + viewType + " " + args);
dump(); dump();
@ -80,10 +80,10 @@ public class ViewModelMessages extends ViewModel {
BoundaryCallbackMessages boundary = null; BoundaryCallbackMessages boundary = null;
if (viewType == AdapterMessage.ViewType.FOLDER) if (viewType == AdapterMessage.ViewType.FOLDER)
boundary = new BoundaryCallbackMessages(context, boundary = new BoundaryCallbackMessages(context,
args.account, args.folder, true, args.query, REMOTE_PAGE_SIZE); args.account, args.folder, true, args.criteria, REMOTE_PAGE_SIZE);
else if (viewType == AdapterMessage.ViewType.SEARCH) else if (viewType == AdapterMessage.ViewType.SEARCH)
boundary = new BoundaryCallbackMessages(context, boundary = new BoundaryCallbackMessages(context,
args.account, args.folder, args.server, args.query, args.account, args.folder, args.server, args.criteria,
args.server ? REMOTE_PAGE_SIZE : SEARCH_PAGE_SIZE); args.server ? REMOTE_PAGE_SIZE : SEARCH_PAGE_SIZE);
LivePagedListBuilder<Integer, TupleMessageEx> builder = null; LivePagedListBuilder<Integer, TupleMessageEx> builder = null;
@ -324,7 +324,7 @@ public class ViewModelMessages extends ViewModel {
private long folder; private long folder;
private String thread; private String thread;
private long id; private long id;
private String query; private BoundaryCallbackMessages.SearchCriteria criteria;
private boolean server; private boolean server;
private boolean threading; private boolean threading;
@ -342,7 +342,7 @@ public class ViewModelMessages extends ViewModel {
AdapterMessage.ViewType viewType, AdapterMessage.ViewType viewType,
String type, long account, long folder, String type, long account, long folder,
String thread, long id, boolean filter_archive, String thread, long id, boolean filter_archive,
String query, boolean server) { BoundaryCallbackMessages.SearchCriteria criteria, boolean server) {
this.type = type; this.type = type;
this.account = account; this.account = account;
@ -350,7 +350,7 @@ public class ViewModelMessages extends ViewModel {
this.thread = thread; this.thread = thread;
this.id = id; this.id = id;
this.filter_archive = filter_archive; this.filter_archive = filter_archive;
this.query = query; this.criteria = criteria;
this.server = server; this.server = server;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@ -381,7 +381,7 @@ public class ViewModelMessages extends ViewModel {
this.folder == other.folder && this.folder == other.folder &&
Objects.equals(this.thread, other.thread) && Objects.equals(this.thread, other.thread) &&
this.id == other.id && this.id == other.id &&
Objects.equals(this.query, other.query) && Objects.equals(this.criteria, other.criteria) &&
this.server == other.server && this.server == other.server &&
this.threading == other.threading && this.threading == other.threading &&
@ -403,7 +403,7 @@ public class ViewModelMessages extends ViewModel {
public String toString() { public String toString() {
return "folder=" + type + ":" + account + ":" + folder + return "folder=" + type + ":" + account + ":" + folder +
" thread=" + thread + ":" + id + " thread=" + thread + ":" + id +
" query=" + query + ":" + server + "" + " criteria=" + criteria + ":" + server + "" +
" threading=" + threading + " threading=" + threading +
" sort=" + sort + ":" + ascending + " sort=" + sort + ":" + ascending +
" filter seen=" + filter_seen + " filter seen=" + filter_seen +

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<eu.faircode.email.FixedTextView
android:id="@+id/tvCaption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_search"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<AutoCompleteTextView
android:id="@+id/etQuery"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:completionThreshold="2"
android:hint="@string/title_search_for_hint"
android:imeOptions="actionGo"
android:inputType="text"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCaption" />
<CheckBox
android:id="@+id/cbSearchIndex"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_search_use_index"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etQuery" />
<CheckBox
android:id="@+id/cbSenders"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/title_search_in_senders"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSearchIndex" />
<CheckBox
android:id="@+id/cbRecipients"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/title_search_in_recipients"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSenders" />
<CheckBox
android:id="@+id/cbSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/title_search_in_subject"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbRecipients" />
<CheckBox
android:id="@+id/cbKeywords"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/title_search_in_keywords"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSubject" />
<CheckBox
android:id="@+id/cbMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/title_search_in_message"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbKeywords" />
<eu.faircode.email.FixedTextView
android:id="@+id/tvAnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_search_with"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbMessage" />
<CheckBox
android:id="@+id/cbUnseen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_search_with_unseen"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAnd" />
<CheckBox
android:id="@+id/cbFlagged"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_search_with_flagged"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbUnseen" />
<CheckBox
android:id="@+id/cbHidden"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_search_with_hidden"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbFlagged" />
<CheckBox
android:id="@+id/cbEncrypted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_search_with_encrypted"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbHidden" />
<CheckBox
android:id="@+id/cbAttachments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_search_with_attachments"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbEncrypted" />
<eu.faircode.email.FixedTextView
android:id="@+id/tvHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_search_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbAttachments" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

@ -6,8 +6,7 @@
android:id="@+id/menu_search" android:id="@+id/menu_search"
android:icon="@drawable/baseline_search_24" android:icon="@drawable/baseline_search_24"
android:title="@string/title_search" android:title="@string/title_search"
app:actionViewClass="eu.faircode.email.SearchViewEx" app:showAsAction="always" />
app:showAsAction="collapseActionView|always" />
<item <item
android:id="@+id/menu_force_sync" android:id="@+id/menu_force_sync"

@ -6,8 +6,7 @@
android:id="@+id/menu_search" android:id="@+id/menu_search"
android:icon="@drawable/baseline_search_24" android:icon="@drawable/baseline_search_24"
android:title="@string/title_search" android:title="@string/title_search"
app:actionViewClass="eu.faircode.email.SearchViewEx" app:showAsAction="always" />
app:showAsAction="collapseActionView|always" />
<item <item
android:id="@+id/menu_compact" android:id="@+id/menu_compact"

@ -6,8 +6,7 @@
android:id="@+id/menu_search" android:id="@+id/menu_search"
android:icon="@drawable/baseline_search_24" android:icon="@drawable/baseline_search_24"
android:title="@string/title_search" android:title="@string/title_search"
app:actionViewClass="eu.faircode.email.SearchViewEx" app:showAsAction="always" />
app:showAsAction="collapseActionView|always" />
<item <item
android:id="@+id/menu_folders" android:id="@+id/menu_folders"

@ -903,9 +903,26 @@
<string name="title_signature_store">Store</string> <string name="title_signature_store">Store</string>
<string name="title_search">Search</string> <string name="title_search">Search</string>
<string name="title_search_for_hint">Enter text</string>
<string name="title_search_use_index">Use search index</string>
<string name="title_search_in_senders">In senders (from)</string>
<string name="title_search_in_recipients">In recipients (to, cc)</string>
<string name="title_search_in_subject">In subject</string>
<string name="title_search_in_keywords">In keyword (if supported)</string>
<string name="title_search_in_message">In message text</string>
<string name="title_search_with">And</string>
<string name="title_search_with_unseen">Unread</string>
<string name="title_search_with_flagged">Starred</string>
<string name="title_search_with_hidden">Hidden (on device only)</string>
<string name="title_search_with_encrypted">Encrypted (on device only)</string>
<string name="title_search_with_attachments">With attachments (on device only)</string>
<string name="title_search_hint">
Searching will initially look at messages stored on your device.
To search the server too, tap on the "search again" button.
</string>
<string name="title_search_device">Search on device</string> <string name="title_search_device">Search on device</string>
<string name="title_search_server">Search on server</string> <string name="title_search_server">Search on server</string>
<string name="title_search_text">Search in text</string>
<string name="title_search_in">Search in</string> <string name="title_search_in">Search in</string>
<string name="title_sort_on">Sort on</string> <string name="title_sort_on">Sort on</string>
@ -1215,13 +1232,6 @@
<string name="title_crash_info_remark">Please describe what you were doing when the app crashed:</string> <string name="title_crash_info_remark">Please describe what you were doing when the app crashed:</string>
<string name="title_issue_subject" translatable="false">FairEmail %1$s issue</string> <string name="title_issue_subject" translatable="false">FairEmail %1$s issue</string>
<string name="title_search_special_prefix">special</string>
<string name="title_search_special_unseen">unread</string>
<string name="title_search_special_flagged">starred</string>
<string name="title_search_special_snoozed">hidden</string>
<string name="title_search_special_encrypted">encrypted</string>
<string name="title_search_special_attachments">attachments</string>
<string name="title_widget_title_count">New message count</string> <string name="title_widget_title_count">New message count</string>
<string name="title_widget_title_list">Message list</string> <string name="title_widget_title_list">Message list</string>

Loading…
Cancel
Save