mirror of
https://github.com/M66B/FairEmail.git
synced 2025-01-01 04:35:57 +00:00
Draft new search
This commit is contained in:
parent
b0ae99b216
commit
adb6d9658e
13 changed files with 637 additions and 476 deletions
|
@ -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 && responses[responses.length - 1].isOK()) {
|
||||
Log.i("Boundary UTF8 search=" + query);
|
||||
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]);
|
||||
|
||||
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]);
|
||||
Log.i("Boundary search=" + criteria);
|
||||
|
||||
// Assume no UTF-8 support
|
||||
String search = query.replace("ß", "ss"); // Eszett
|
||||
search = Normalizer.normalize(search, Normalizer.Form.NFD)
|
||||
.replaceAll("[^\\p{ASCII}]", "");
|
||||
List<SearchTerm> or = new ArrayList<>();
|
||||
List<SearchTerm> and = new ArrayList<>();
|
||||
if (criteria.query != null) {
|
||||
String search = criteria.query;
|
||||
|
||||
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));
|
||||
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();
|
||||
|
|
171
app/src/main/java/eu/faircode/email/FragmentDialogSearch.java
Normal file
171
app/src/main/java/eu/faircode/email/FragmentDialogSearch.java
Normal file
|
@ -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 +
|
||||
|
|
182
app/src/main/res/layout/dialog_search.xml
Normal file
182
app/src/main/res/layout/dialog_search.xml
Normal file
|
@ -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…
Reference in a new issue