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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagedList;
import androidx.preference.PreferenceManager;
import com.sun.mail.iap.Argument;
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
@ -38,11 +40,12 @@ import com.sun.mail.imap.protocol.IMAPResponse;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.io.Serializable;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import javax.mail.FetchProfile;
@ -61,6 +64,7 @@ import javax.mail.search.FlagTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SearchException;
import javax.mail.search.SearchTerm;
import javax.mail.search.SubjectTerm;
@ -71,7 +75,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private Long account;
private Long folder;
private boolean server;
private String query;
private SearchCriteria criteria;
private int pageSize;
private IBoundaryCallbackMessages intf;
@ -91,12 +95,12 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
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.account = (account < 0 ? null : account);
this.folder = (folder < 0 ? null : folder);
this.server = server;
this.query = query;
this.criteria = criteria;
this.pageSize = pageSize;
}
@ -182,35 +186,15 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private int load_device(State state) {
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;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean fts = prefs.getBoolean("fts", false);
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) {
SQLiteDatabase sdb = FtsDbHelper.getInstance(context);
state.ids = FtsDbHelper.match(sdb, account, folder, query);
state.ids = FtsDbHelper.match(sdb, account, folder, criteria.query);
}
try {
@ -237,16 +221,15 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
(state.matches.size() > 0 && state.index >= state.matches.size())) {
state.matches = db.message().matchMessages(
account, folder,
"%" + find + "%",
seen, flagged, snoozed, encrypted, attachments,
criteria.query == null ? null : "%" + criteria.query + "%",
criteria.with_unseen,
criteria.with_flagged,
criteria.with_hidden,
criteria.with_encrypted,
criteria.with_attachments,
SEARCH_LIMIT, state.offset);
Log.i("Boundary device folder=" + folder +
" query=" + query +
" seen=" + seen +
" flagged=" + flagged +
" snoozed=" + snoozed +
" encrypted=" + encrypted +
" attachments=" + attachments +
" criteria=" + criteria +
" offset=" + state.offset +
" size=" + state.matches.size());
state.offset += Math.min(state.matches.size(), SEARCH_LIMIT);
@ -260,25 +243,20 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
state.index = i + 1;
TupleMatch match = state.matches.get(i);
if (find == null || seen != null || flagged != null || snoozed != null || encrypted != null || attachments != null)
match.matched = true;
else {
if (match.matched == null || !match.matched)
try {
File file = EntityMessage.getFile(context, match.id);
if (file.exists()) {
String html = Helper.readText(file);
if (html.toLowerCase().contains(find)) {
String text = HtmlHelper.getFullText(html);
if (text.toLowerCase().contains(find))
match.matched = true;
}
if (criteria.query != null && (match.matched == null || !match.matched))
try {
File file = EntityMessage.getFile(context, match.id);
if (file.exists()) {
String html = Helper.readText(file);
if (html.toLowerCase().contains(criteria.query)) {
String text = HtmlHelper.getFullText(html);
if (text.toLowerCase().contains(criteria.query))
match.matched = true;
}
} catch (IOException ex) {
Log.e(ex);
}
}
} catch (IOException ex) {
Log.e(ex);
}
if (match.matched != null && match.matched) {
found++;
@ -303,10 +281,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
DB db = DB.getInstance(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 EntityFolder browsable = db.folder().getBrowsableFolder(folder, query != null);
final EntityFolder browsable = db.folder().getBrowsableFolder(folder, criteria != null);
if (browsable == null || !browsable.selectable) {
Log.w("Boundary not browsable=" + (folder != null));
return 0;
@ -344,8 +321,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
int count = state.ifolder.getMessageCount();
db.folder().setFolderTotal(browsable.id, count < 0 ? null : count);
Log.i("Boundary server query=" + query);
if (query == null) {
Log.i("Boundary server query=" + criteria.query);
if (criteria == null) {
boolean filter_seen = prefs.getBoolean("filter_seen", false);
boolean filter_unflagged = prefs.getBoolean("filter_unflagged", false);
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);
else
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 {
Object result = state.ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol protocol) {
// 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;
}
public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
try {
// https://tools.ietf.org/html/rfc3501#section-6.4.4
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://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw
arg.writeAtom("X-GM-RAW");
arg.writeString(query.substring(4));
} else {
if (!protocol.supportsUtf8()) {
arg.writeAtom("CHARSET");
arg.writeAtom(StandardCharsets.UTF_8.name());
}
arg.writeAtom("OR");
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());
}
}
arg.writeString(criteria.query.substring(4));
Response[] responses = protocol.command("SEARCH", arg);
if (responses.length == 0)
throw new ProtocolException("No response");
if (!responses[responses.length - 1].isOK())
throw new ProtocolException(responses[responses.length - 1]);
Response[] responses = protocol.command("SEARCH", arg);
if (responses.length > 0 && responses[responses.length - 1].isOK()) {
Log.i("Boundary UTF8 search=" + query);
Log.i("Boundary raw search=" + criteria.query);
List<Integer> msgnums = new ArrayList<>();
for (Response response : responses)
@ -442,37 +380,68 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return imessages;
} else {
if (responses.length > 0)
Log.e("Search response=" + responses[responses.length - 1]);
// Assume no UTF-8 support
String search = query.replace("ß", "ss"); // Eszett
search = Normalizer.normalize(search, Normalizer.Form.NFD)
.replaceAll("[^\\p{ASCII}]", "");
Log.i("Boundary ASCII search=" + search);
SearchTerm term = new FromStringTerm(search);
term = new OrTerm(term, new RecipientStringTerm(Message.RecipientType.TO, search));
term = new OrTerm(term, new RecipientStringTerm(Message.RecipientType.CC, search));
term = new OrTerm(term, new SubjectTerm(search));
if (search_text)
term = new OrTerm(term, new BodyTerm(search));
if (keywords)
term = new OrTerm(term, new FlagTerm(
new Flags(MessageHelper.sanitizeKeyword(search)), true));
Log.i("Boundary search=" + criteria);
List<SearchTerm> or = new ArrayList<>();
List<SearchTerm> and = new ArrayList<>();
if (criteria.query != null) {
String search = criteria.query;
if (!protocol.supportsUtf8()) {
search = search.replace("ß", "ss"); // Eszett
search = Normalizer.normalize(search, Normalizer.Form.NFD)
.replaceAll("[^\\p{ASCII}]", "");
}
// 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;
}
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);
}
} catch (MessagingException ex) {
Log.e(ex);
return ex;
throw new ProtocolException("Search", ex);
}
}
});
if (result instanceof MessagingException)
throw (MessagingException) result;
state.imessages = (Message[]) result;
}
Log.i("Boundary server found messages=" + state.imessages.length);
@ -541,7 +510,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
rules, astate);
found++;
}
if (message != null && query != null /* browsed */)
if (message != null && criteria != null /* browsed */)
db.message().setMessageFound(message.id);
} catch (MessageRemovedException ex) {
Log.w(browsable.name + " boundary server", ex);
@ -623,4 +592,87 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
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" +
" AND (:account IS NULL OR account = :account)" +
" AND (:folder IS NULL OR folder = :folder)" +
" AND (:seen IS NULL OR ui_seen = :seen)" +
" AND (:flagged IS NULL OR ui_flagged = :flagged)" +
" AND (:hidden IS NULL OR (CASE WHEN ui_snoozed IS NULL THEN 0 ELSE 1 END) = :hidden)" +
" AND (:encrypted IS NULL OR ui_encrypt > 0)" +
" AND (:attachments IS NULL OR attachments > 0)" +
" AND (NOT :unseen OR NOT ui_seen)" +
" AND (NOT :flagged OR ui_flagged)" +
" AND (NOT :hidden OR NOT ui_snoozed IS NULL)" +
" AND (NOT :encrypted OR ui_encrypt > 0)" +
" AND (NOT :attachments OR attachments > 0)" +
" ORDER BY received DESC" +
" LIMIT :limit OFFSET :offset")
List<TupleMatch> matchMessages(
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);
@Query("SELECT id" +

@ -71,7 +71,6 @@ public class FragmentAccounts extends FragmentBase {
private FloatingActionButton fabCompose;
private ObjectAnimator animator;
private String searching = null;
private AdapterAccount adapter;
private static final int REQUEST_IMPORT_OAUTH = 1;
@ -247,19 +246,10 @@ public class FragmentAccounts extends FragmentBase {
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("fair:searching", searching);
super.onSaveInstanceState(outState);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
searching = savedInstanceState.getString("fair:searching");
DB db = DB.getInstance(getContext());
// Observe accounts
@ -294,23 +284,6 @@ public class FragmentAccounts extends FragmentBase {
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
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);
}
@ -324,6 +297,9 @@ public class FragmentAccounts extends FragmentBase {
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_search:
onMenuSearch();
return true;
case R.id.menu_force_sync:
onMenuForceSync();
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() {
ServiceSynchronize.reload(getContext(), null, true, "force sync");
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 boolean primary;
private boolean show_hidden = false;
private String searching = null;
private AdapterFolder adapter;
private NumberFormat NF = NumberFormat.getNumberInstance();
@ -263,20 +262,10 @@ public class FragmentFolders extends FragmentBase {
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("fair:searching", searching);
super.onSaveInstanceState(outState);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
searching = savedInstanceState.getString("fair:searching");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
grpHintActions.setVisibility(prefs.getBoolean("folder_actions", 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
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
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);
}
@ -466,6 +438,9 @@ public class FragmentFolders extends FragmentBase {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_search:
onMenuSearch();
return true;
case R.id.menu_compact:
onMenuCompact();
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() {
compact = !compact;

@ -247,7 +247,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
private long id;
private boolean filter_archive;
private boolean found;
private String query;
private BoundaryCallbackMessages.SearchCriteria criteria = null;
private boolean pane;
private long message = -1;
@ -272,7 +272,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
private long primary;
private boolean connected;
private boolean reset = false;
private String searching = null;
private boolean initialized = false;
private boolean loading = false;
private boolean swiping = false;
@ -356,12 +355,12 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
id = args.getLong("id", -1);
filter_archive = args.getBoolean("filter_archive", true);
found = args.getBoolean("found", false);
query = args.getString("query");
criteria = (BoundaryCallbackMessages.SearchCriteria) args.getSerializable("criteria");
pane = args.getBoolean("pane", false);
primary = args.getLong("primary", -1);
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");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
@ -382,7 +381,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
colorPrimary = Helper.resolveColor(getContext(), R.attr.colorPrimary);
colorAccent = Helper.resolveColor(getContext(), R.attr.colorAccent);
if (TextUtils.isEmpty(query))
if (criteria == null)
if (thread == null) {
if (folder < 0)
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);
popupMenu.getMenu().add(Menu.NONE, 0, order++, ss)
.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);
if (!TextUtils.isEmpty(folderName))
popupMenu.getMenu().add(Menu.NONE, 2, order++, folderName);
popupMenu.getMenu().add(Menu.NONE, 1, order++, folderName);
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));
popupMenu.getMenu().add(Menu.NONE, 999, order++, R.string.title_setup_help);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {
switch (target.getItemId()) {
case 1: // Search text
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean search_text = prefs.getBoolean("search_text", false);
prefs.edit().putBoolean("search_text", !search_text).apply();
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;
if (target.getItemId() == 1) { // Search same folder
search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
account, folder, true, criteria);
return true;
}
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.putLong("account", intent.getLongExtra("account", -1));
args.putLongArray("disabled", new long[]{});
args.putString("query", query);
args.putSerializable("criteria", criteria);
FragmentDialogFolder fragment = new FragmentDialogFolder();
fragment.setArguments(args);
@ -1122,16 +1104,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
else
fabCompose.hide();
if (viewType == AdapterMessage.ViewType.SEARCH && !server) {
if (query != null && query.startsWith(getString(R.string.title_search_special_prefix) + ":")) {
String special = query.split(":")[1];
if (getString(R.string.title_search_special_snoozed).equals(special) ||
getString(R.string.title_search_special_encrypted).equals(special) ||
getString(R.string.title_search_special_attachments).equals(special))
fabSearch.hide();
else
fabSearch.show();
} else
if (viewType == AdapterMessage.ViewType.SEARCH && criteria != null && !server) {
if (criteria.with_hidden || criteria.with_encrypted || criteria.with_attachments)
fabSearch.hide();
else
fabSearch.show();
} else
fabSearch.hide();
@ -2949,8 +2925,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean("fair:reset", reset);
outState.putString("fair:searching", searching);
outState.putBoolean("fair:autoExpanded", autoExpanded);
outState.putInt("fair:autoCloseCount", autoCloseCount);
@ -2975,8 +2949,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
if (savedInstanceState != null) {
reset = savedInstanceState.getBoolean("fair:reset");
searching = savedInstanceState.getString("fair:searching");
autoExpanded = savedInstanceState.getBoolean("fair:autoExpanded");
autoCloseCount = savedInstanceState.getInt("fair:autoCloseCount");
@ -3108,7 +3080,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
break;
case SEARCH:
setSubtitle(query);
setSubtitle(criteria.getTitle());
break;
}
@ -3347,22 +3319,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
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);
ImageButton ib = (ImageButton) menu.findItem(R.id.menu_folders).getActionView();
ib.setImageResource(R.drawable.baseline_folder_24);
@ -3414,8 +3370,6 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
menuSearch.setVisible(
(viewType == AdapterMessage.ViewType.UNIFIED && type == null)
|| viewType == AdapterMessage.ViewType.FOLDER);
if (!menuSearch.isVisible())
menuSearch.collapseActionView();
menu.findItem(R.id.menu_folders).setVisible(viewType == AdapterMessage.ViewType.UNIFIED && primary >= 0);
ImageButton ib = (ImageButton) menu.findItem(R.id.menu_folders).getActionView();
@ -3494,6 +3448,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_search:
onMenuSearch();
return true;
case R.id.menu_folders:
// Obsolete
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) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
getParentFragmentManager().popBackStack("unified", 0);
@ -3887,7 +3855,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
ViewModelMessages.Model vmodel = model.getModel(
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.setObserver(getViewLifecycleOwner(), observer);
@ -4969,12 +4937,12 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
case REQUEST_SEARCH:
if (resultCode == RESULT_OK && data != null) {
Bundle args = data.getBundleExtra("args");
search(
getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
BoundaryCallbackMessages.SearchCriteria criteria =
(BoundaryCallbackMessages.SearchCriteria) args.getSerializable("criteria");
search(getContext(), getViewLifecycleOwner(), getParentFragmentManager(),
args.getLong("account"),
args.getLong("folder"),
true,
args.getString("query"));
true, criteria);
}
break;
case REQUEST_ACCOUNT:
@ -6506,6 +6474,14 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
static void search(
final Context context, final LifecycleOwner owner, final FragmentManager manager,
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)) {
context.startActivity(new Intent(context, ActivityBilling.class));
return;
@ -6518,7 +6494,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
args.putLong("account", account);
args.putLong("folder", folder);
args.putBoolean("server", server);
args.putString("query", query);
args.putSerializable("criteria", criteria);
FragmentMessages fragment = new FragmentMessages();
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,
String type, long account, long folder,
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);
dump();
@ -80,10 +80,10 @@ public class ViewModelMessages extends ViewModel {
BoundaryCallbackMessages boundary = null;
if (viewType == AdapterMessage.ViewType.FOLDER)
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)
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);
LivePagedListBuilder<Integer, TupleMessageEx> builder = null;
@ -324,7 +324,7 @@ public class ViewModelMessages extends ViewModel {
private long folder;
private String thread;
private long id;
private String query;
private BoundaryCallbackMessages.SearchCriteria criteria;
private boolean server;
private boolean threading;
@ -342,7 +342,7 @@ public class ViewModelMessages extends ViewModel {
AdapterMessage.ViewType viewType,
String type, long account, long folder,
String thread, long id, boolean filter_archive,
String query, boolean server) {
BoundaryCallbackMessages.SearchCriteria criteria, boolean server) {
this.type = type;
this.account = account;
@ -350,7 +350,7 @@ public class ViewModelMessages extends ViewModel {
this.thread = thread;
this.id = id;
this.filter_archive = filter_archive;
this.query = query;
this.criteria = criteria;
this.server = server;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@ -381,7 +381,7 @@ public class ViewModelMessages extends ViewModel {
this.folder == other.folder &&
Objects.equals(this.thread, other.thread) &&
this.id == other.id &&
Objects.equals(this.query, other.query) &&
Objects.equals(this.criteria, other.criteria) &&
this.server == other.server &&
this.threading == other.threading &&
@ -403,7 +403,7 @@ public class ViewModelMessages extends ViewModel {
public String toString() {
return "folder=" + type + ":" + account + ":" + folder +
" thread=" + thread + ":" + id +
" query=" + query + ":" + server + "" +
" criteria=" + criteria + ":" + server + "" +
" threading=" + threading +
" sort=" + sort + ":" + ascending +
" 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:icon="@drawable/baseline_search_24"
android:title="@string/title_search"
app:actionViewClass="eu.faircode.email.SearchViewEx"
app:showAsAction="collapseActionView|always" />
app:showAsAction="always" />
<item
android:id="@+id/menu_force_sync"

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

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

@ -903,9 +903,26 @@
<string name="title_signature_store">Store</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_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_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_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_list">Message list</string>

Loading…
Cancel
Save