Added saved searches

This commit is contained in:
M66B 2021-09-24 13:36:06 +02:00
parent be1865387d
commit c15943144b
13 changed files with 3284 additions and 92 deletions

View File

@ -6,7 +6,7 @@
### Next version
* Storing folder namespaces
* Added saved searches
### 1.1732

View File

@ -4,7 +4,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'de.undercouch.download'
def getVersionCode = { -> return 1732 }
def getRevision = { -> "\"a\"" }
def getRevision = { -> "\"b\"" }
def getReleaseName = { -> return "\"Zanabazar\"" }
// https://en.wikipedia.org/wiki/List_of_dinosaur_genera

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
### Next version
* Storing folder namespaces
* Added saved searches
### 1.1732

View File

@ -114,19 +114,30 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
private ImageButton ibSettings;
private ImageButton ibFetchMore;
private ImageButton ibForceSync;
private View vSeparatorOptions;
private ImageButton ibExpanderAccount;
private RecyclerView rvAccount;
private ImageButton ibExpanderUnified;
private ImageButton ibExpanderSearch;
private RecyclerView rvSearch;
private View vSeparatorSearch;
private RecyclerView rvUnified;
private ImageButton ibExpanderMenu;
private RecyclerView rvMenu;
private ImageButton ibExpanderExtra;
private RecyclerView rvMenuExtra;
private Group grpOptions;
private AdapterNavAccountFolder adapterNavAccount;
private AdapterNavUnified adapterNavUnified;
private AdapterNavSearch adapterNavSearch;
private AdapterNavMenu adapterNavMenu;
private AdapterNavMenu adapterNavMenuExtra;
@ -291,12 +302,20 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
ibForceSync = drawerContainer.findViewById(R.id.ibForceSync);
vSeparatorOptions = drawerContainer.findViewById(R.id.vSeparatorOptions);
grpOptions = drawerContainer.findViewById(R.id.grpOptions);
ibExpanderAccount = drawerContainer.findViewById(R.id.ibExpanderAccount);
rvAccount = drawerContainer.findViewById(R.id.rvAccount);
ibExpanderUnified = drawerContainer.findViewById(R.id.ibExpanderUnified);
rvUnified = drawerContainer.findViewById(R.id.rvUnified);
ibExpanderSearch = drawerContainer.findViewById(R.id.ibExpanderSearch);
rvSearch = drawerContainer.findViewById(R.id.rvSearch);
vSeparatorSearch = drawerContainer.findViewById(R.id.vSeparatorSearch);
ibExpanderMenu = drawerContainer.findViewById(R.id.ibExpanderMenu);
rvMenu = drawerContainer.findViewById(R.id.rvMenu);
ibExpanderExtra = drawerContainer.findViewById(R.id.ibExpanderExtra);
rvMenuExtra = drawerContainer.findViewById(R.id.rvMenuExtra);
@ -473,6 +492,27 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
}
});
// Menus
rvSearch.setLayoutManager(new LinearLayoutManager(this));
adapterNavSearch = new AdapterNavSearch(this, this, getSupportFragmentManager());
rvSearch.setAdapter(adapterNavSearch);
boolean nav_search = prefs.getBoolean("nav_search", true);
ibExpanderSearch.setImageLevel(nav_search ? 0 /* less */ : 1 /* more */);
ibExpanderSearch.setVisibility(View.GONE);
rvSearch.setVisibility(View.GONE);
vSeparatorSearch.setVisibility(View.GONE);
ibExpanderSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean nav_search = !prefs.getBoolean("nav_search", true);
prefs.edit().putBoolean("nav_search", nav_search).apply();
ibExpanderSearch.setImageLevel(nav_search ? 0 /* less */ : 1 /* more */);
rvSearch.setVisibility(nav_search ? View.VISIBLE : View.GONE);
}
});
// Menus
rvMenu.setLayoutManager(new LinearLayoutManager(this));
adapterNavMenu = new AdapterNavMenu(this, this);
@ -784,6 +824,20 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
}
});
db.search().liveSearch().observe(owner, new Observer<List<EntitySearch>>() {
@Override
public void onChanged(List<EntitySearch> search) {
if (search == null)
search = new ArrayList<>();
adapterNavSearch.set(search, nav_expanded);
boolean nav_search = prefs.getBoolean("nav_search", true);
ibExpanderSearch.setVisibility(search.size() > 0 ? View.VISIBLE : View.GONE);
rvSearch.setVisibility(search.size() > 0 && nav_search ? View.VISIBLE : View.GONE);
vSeparatorSearch.setVisibility(search.size() > 0 ? View.VISIBLE : View.GONE);
}
});
db.operation().liveStats().observe(owner, new Observer<TupleOperationStats>() {
@Override
public void onChanged(TupleOperationStats stats) {

View File

@ -0,0 +1,267 @@
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-2021 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public class AdapterNavSearch extends RecyclerView.Adapter<AdapterNavSearch.ViewHolder> {
private Context context;
private LifecycleOwner owner;
private FragmentManager manager;
private LayoutInflater inflater;
private boolean expanded = true;
private List<EntitySearch> items = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
private View view;
private ImageView ivItem;
private ImageView ivBadge;
private TextView tvItem;
private TextView tvItemExtra;
private ImageView ivExtra;
private ImageView ivWarning;
ViewHolder(View itemView) {
super(itemView);
view = itemView.findViewById(R.id.clItem);
ivItem = itemView.findViewById(R.id.ivItem);
ivBadge = itemView.findViewById(R.id.ivBadge);
tvItem = itemView.findViewById(R.id.tvItem);
tvItemExtra = itemView.findViewById(R.id.tvItemExtra);
ivExtra = itemView.findViewById(R.id.ivExtra);
ivWarning = itemView.findViewById(R.id.ivWarning);
}
private void wire() {
view.setOnClickListener(this);
view.setOnLongClickListener(this);
}
private void unwire() {
view.setOnClickListener(null);
view.setOnLongClickListener(null);
}
private void bindTo(EntitySearch search) {
ivItem.setImageResource(R.drawable.twotone_search_24);
ivBadge.setVisibility(View.GONE);
tvItem.setText(search.name);
tvItemExtra.setVisibility(View.GONE);
ivExtra.setVisibility(View.GONE);
ivWarning.setVisibility(View.GONE);
}
@Override
public void onClick(View v) {
int pos = getAdapterPosition();
if (pos == RecyclerView.NO_POSITION)
return;
EntitySearch search = items.get(pos);
if (search == null)
return;
try {
JSONObject json = new JSONObject(search.data);
BoundaryCallbackMessages.SearchCriteria criteria =
BoundaryCallbackMessages.SearchCriteria.fromJSON(json);
FragmentMessages.search(
context, owner, manager,
-1L, -1L, false, criteria);
} catch (Throwable ex) {
Log.e(ex);
}
}
@Override
public boolean onLongClick(View v) {
int pos = getAdapterPosition();
if (pos == RecyclerView.NO_POSITION)
return false;
EntitySearch search = items.get(pos);
if (search == null)
return false;
Bundle args = new Bundle();
args.putLong("id", search.id);
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
DB db = DB.getInstance(context);
db.search().deleteSearch(id);
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(manager, ex);
}
}.execute(context, owner, args, "search:delete");
return true;
}
}
AdapterNavSearch(Context context, LifecycleOwner owner, FragmentManager manager) {
this.context = context;
this.owner = owner;
this.manager = manager;
this.inflater = LayoutInflater.from(context);
setHasStableIds(true);
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
Log.d(AdapterNavSearch.this + " parent destroyed");
AdapterNavSearch.this.manager = null;
}
});
}
public void set(@NonNull List<EntitySearch> search, boolean expanded) {
Log.i("Set nav search=" + search.size() + " expanded=" + expanded);
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, search), false);
this.expanded = expanded;
this.items = search;
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.d("Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.d("Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.d("Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.d("Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(this);
}
public void setExpanded(boolean expanded) {
this.expanded = expanded;
notifyDataSetChanged();
}
EntitySearch get(int pos) {
return items.get(pos);
}
private static class DiffCallback extends DiffUtil.Callback {
private List<EntitySearch> prev = new ArrayList<>();
private List<EntitySearch> next = new ArrayList<>();
DiffCallback(List<EntitySearch> prev, List<EntitySearch> next) {
this.prev.addAll(prev);
this.next.addAll(next);
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
EntitySearch s1 = prev.get(oldItemPosition);
EntitySearch s2 = next.get(newItemPosition);
return s1.id.equals(s2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntitySearch s1 = prev.get(oldItemPosition);
EntitySearch s2 = next.get(newItemPosition);
return s1.equals(s2);
}
}
@Override
public long getItemId(int position) {
return items.get(position).id;
}
@Override
public int getItemCount() {
return items.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.item_nav, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
EntitySearch search = items.get(position);
holder.bindTo(search);
holder.wire();
}
}

View File

@ -39,6 +39,10 @@ import com.sun.mail.imap.protocol.IMAPResponse;
import com.sun.mail.imap.protocol.SearchSequence;
import com.sun.mail.util.MessageRemovedIOException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
@ -953,6 +957,86 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return false;
}
JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("query", query);
json.put("in_senders", in_senders);
json.put("in_recipients", in_recipients);
json.put("in_subject", in_subject);
json.put("in_keywords", in_keywords);
json.put("in_message", in_message);
json.put("in_notes", in_notes);
json.put("in_headers", in_headers);
json.put("in_html", in_html);
json.put("with_unseen", with_unseen);
json.put("with_flagged", with_flagged);
json.put("with_hidden", with_hidden);
json.put("with_encrypted", with_encrypted);
json.put("with_attachments", with_attachments);
json.put("with_notes", with_notes);
if (with_types != null) {
JSONArray jtypes = new JSONArray();
for (String type : with_types)
jtypes.put(type);
json.put("with_types", jtypes);
}
if (with_size != null)
json.put("with_size", with_size);
json.put("in_trash", in_trash);
json.put("in_junk", in_junk);
if (after != null)
json.put("after", after);
if (before != null)
json.put("before", before);
return json;
}
public static SearchCriteria fromJSON(JSONObject json) throws JSONException {
SearchCriteria criteria = new SearchCriteria();
criteria.query = json.optString("query");
criteria.in_senders = json.optBoolean("in_senders");
criteria.in_recipients = json.optBoolean("in_recipients");
criteria.in_subject = json.optBoolean("in_subject");
criteria.in_keywords = json.optBoolean("in_keywords");
criteria.in_message = json.optBoolean("in_message");
criteria.in_notes = json.optBoolean("in_notes");
criteria.in_headers = json.optBoolean("in_headers");
criteria.in_html = json.optBoolean("in_html");
criteria.with_unseen = json.optBoolean("with_unseen");
criteria.with_flagged = json.optBoolean("with_flagged");
criteria.with_hidden = json.optBoolean("with_hidden");
criteria.with_encrypted = json.optBoolean("with_encrypted");
criteria.with_attachments = json.optBoolean("with_attachments");
criteria.with_notes = json.optBoolean("with_notes");
if (json.has("with_types")) {
JSONArray jtypes = json.getJSONArray("with_types");
criteria.with_types = new String[jtypes.length()];
for (int i = 0; i < jtypes.length(); i++)
criteria.with_types[i] = jtypes.getString(i);
}
if (json.has("with_size"))
criteria.with_size = json.getInt("with_size");
criteria.in_trash = json.optBoolean("in_trash");
criteria.in_junk = json.optBoolean("in_junk");
if (json.has("after"))
criteria.after = json.getLong("after");
if (json.has("before"))
criteria.before = json.getLong("before");
return criteria;
}
@NonNull
@Override
public String toString() {

View File

@ -68,7 +68,7 @@ import io.requery.android.database.sqlite.SQLiteDatabase;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 210,
version = 211,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -80,6 +80,7 @@ import io.requery.android.database.sqlite.SQLiteDatabase;
EntityCertificate.class,
EntityAnswer.class,
EntityRule.class,
EntitySearch.class,
EntityLog.class
},
views = {
@ -111,6 +112,8 @@ public abstract class DB extends RoomDatabase {
public abstract DaoRule rule();
public abstract DaoSearch search();
public abstract DaoLog log();
private static int sPid;
@ -124,7 +127,7 @@ public abstract class DB extends RoomDatabase {
private static final int DB_CHECKPOINT = 1000; // requery/sqlite-android default
private static final String[] DB_TABLES = new String[]{
"identity", "account", "folder", "message", "attachment", "operation", "contact", "certificate", "answer", "rule", "log"};
"identity", "account", "folder", "message", "attachment", "operation", "contact", "certificate", "answer", "rule", "search", "log"};
@Override
public void init(@NonNull DatabaseConfiguration configuration) {
@ -2143,14 +2146,23 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `log` ADD COLUMN `message` INTEGER");
}
}).addMigrations(new Migration(209, 210) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `folder` ADD COLUMN `namespace` TEXT");
db.execSQL("ALTER TABLE `folder` ADD COLUMN `separator` INTEGER");
db.execSQL("UPDATE folder SET separator =" +
" (SELECT separator FROM account WHERE account.id = folder.account)");
}
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `folder` ADD COLUMN `namespace` TEXT");
db.execSQL("ALTER TABLE `folder` ADD COLUMN `separator` INTEGER");
db.execSQL("UPDATE folder SET separator =" +
" (SELECT separator FROM account WHERE account.id = folder.account)");
}
}).addMigrations(new Migration(210, 211) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("CREATE TABLE `search`" +
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
", name TEXT NOT NULL" +
", `data` TEXT NOT NULL)");
}
}).addMigrations(new Migration(998, 999) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {

View File

@ -0,0 +1,41 @@
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-2021 by Marcel Bokhorst (M66B)
*/
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface DaoSearch {
@Query("SELECT * FROM search" +
" ORDER BY name COLLATE NOCASE")
LiveData<List<EntitySearch>> liveSearch();
@Insert
long insertSearch(EntitySearch search);
@Query("DELETE FROM search" +
" WHERE id = :id")
int deleteSearch(long id);
}

View File

@ -0,0 +1,53 @@
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-2021 by Marcel Bokhorst (M66B)
*/
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(
tableName = EntitySearch.TABLE_NAME,
foreignKeys = {
},
indices = {
}
)
public class EntitySearch {
static final String TABLE_NAME = "search";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public String name;
@NonNull
public String data;
@Override
public boolean equals(Object obj) {
if (obj instanceof EntitySearch) {
EntitySearch other = (EntitySearch) obj;
return (this.id.equals(other.id) &&
this.name.equals(other.name) &&
this.data.equals(other.data));
} else
return false;
}
}

View File

@ -58,6 +58,44 @@ import java.util.List;
import io.requery.android.database.sqlite.SQLiteDatabase;
public class FragmentDialogSearch extends FragmentDialogBase {
private TextViewAutoCompleteAction etQuery;
private TextView tvSearch1;
private TextView tvSearch2;
private TextView tvSearch3;
private ImageButton ibResetSearches;
private ImageButton ibInfo;
private ImageButton ibFlagged;
private ImageButton ibUnseen;
private ImageButton ibInvite;
private ImageButton ibAttachment;
private ImageButton ibNotes;
private ImageButton ibMore;
private TextView tvMore;
private CheckBox cbSearchIndex;
private CheckBox cbSenders;
private CheckBox cbRecipients;
private CheckBox cbSubject;
private CheckBox cbKeywords;
private CheckBox cbMessage;
private TextView tvSearchTextUnsupported;
private CheckBox cbNotes;
private CheckBox cbHeaders;
private CheckBox cbHtml;
private CheckBox cbSearchTrash;
private CheckBox cbSearchJunk;
private CheckBox cbUnseen;
private CheckBox cbFlagged;
private CheckBox cbHidden;
private CheckBox cbEncrypted;
private CheckBox cbAttachments;
private Spinner spMessageSize;
private Button btnBefore;
private Button btnAfter;
private TextView tvBefore;
private TextView tvAfter;
private Group grpMore;
private static final int MAX_SUGGESTIONS = 3;
@NonNull
@ -82,43 +120,43 @@ public class FragmentDialogSearch extends FragmentDialogBase {
View dview = LayoutInflater.from(context).inflate(R.layout.dialog_search, null);
final TextViewAutoCompleteAction etQuery = dview.findViewById(R.id.etQuery);
final TextView tvSearch1 = dview.findViewById(R.id.tvSearch1);
final TextView tvSearch2 = dview.findViewById(R.id.tvSearch2);
final TextView tvSearch3 = dview.findViewById(R.id.tvSearch3);
final ImageButton ibResetSearches = dview.findViewById(R.id.ibResetSearches);
etQuery = dview.findViewById(R.id.etQuery);
tvSearch1 = dview.findViewById(R.id.tvSearch1);
tvSearch2 = dview.findViewById(R.id.tvSearch2);
tvSearch3 = dview.findViewById(R.id.tvSearch3);
ibResetSearches = dview.findViewById(R.id.ibResetSearches);
final ImageButton ibInfo = dview.findViewById(R.id.ibInfo);
final ImageButton ibFlagged = dview.findViewById(R.id.ibFlagged);
final ImageButton ibUnseen = dview.findViewById(R.id.ibUnseen);
final ImageButton ibInvite = dview.findViewById(R.id.ibInvite);
final ImageButton ibAttachment = dview.findViewById(R.id.ibAttachment);
final ImageButton ibNotes = dview.findViewById(R.id.ibNotes);
final ImageButton ibMore = dview.findViewById(R.id.ibMore);
final TextView tvMore = dview.findViewById(R.id.tvMore);
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 TextView tvSearchTextUnsupported = dview.findViewById(R.id.tvSearchTextUnsupported);
final CheckBox cbNotes = dview.findViewById(R.id.cbNotes);
final CheckBox cbHeaders = dview.findViewById(R.id.cbHeaders);
final CheckBox cbHtml = dview.findViewById(R.id.cbHtml);
final CheckBox cbSearchTrash = dview.findViewById(R.id.cbSearchTrash);
final CheckBox cbSearchJunk = dview.findViewById(R.id.cbSearchJunk);
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);
final Spinner spMessageSize = dview.findViewById(R.id.spMessageSize);
final Button btnBefore = dview.findViewById(R.id.btnBefore);
final Button btnAfter = dview.findViewById(R.id.btnAfter);
final TextView tvBefore = dview.findViewById(R.id.tvBefore);
final TextView tvAfter = dview.findViewById(R.id.tvAfter);
final Group grpMore = dview.findViewById(R.id.grpMore);
ibInfo = dview.findViewById(R.id.ibInfo);
ibFlagged = dview.findViewById(R.id.ibFlagged);
ibUnseen = dview.findViewById(R.id.ibUnseen);
ibInvite = dview.findViewById(R.id.ibInvite);
ibAttachment = dview.findViewById(R.id.ibAttachment);
ibNotes = dview.findViewById(R.id.ibNotes);
ibMore = dview.findViewById(R.id.ibMore);
tvMore = dview.findViewById(R.id.tvMore);
cbSearchIndex = dview.findViewById(R.id.cbSearchIndex);
cbSenders = dview.findViewById(R.id.cbSenders);
cbRecipients = dview.findViewById(R.id.cbRecipients);
cbSubject = dview.findViewById(R.id.cbSubject);
cbKeywords = dview.findViewById(R.id.cbKeywords);
cbMessage = dview.findViewById(R.id.cbMessage);
tvSearchTextUnsupported = dview.findViewById(R.id.tvSearchTextUnsupported);
cbNotes = dview.findViewById(R.id.cbNotes);
cbHeaders = dview.findViewById(R.id.cbHeaders);
cbHtml = dview.findViewById(R.id.cbHtml);
cbSearchTrash = dview.findViewById(R.id.cbSearchTrash);
cbSearchJunk = dview.findViewById(R.id.cbSearchJunk);
cbUnseen = dview.findViewById(R.id.cbUnseen);
cbFlagged = dview.findViewById(R.id.cbFlagged);
cbHidden = dview.findViewById(R.id.cbHidden);
cbEncrypted = dview.findViewById(R.id.cbEncrypted);
cbAttachments = dview.findViewById(R.id.cbAttachments);
spMessageSize = dview.findViewById(R.id.spMessageSize);
btnBefore = dview.findViewById(R.id.btnBefore);
btnAfter = dview.findViewById(R.id.btnAfter);
tvBefore = dview.findViewById(R.id.tvBefore);
tvAfter = dview.findViewById(R.id.tvAfter);
grpMore = dview.findViewById(R.id.grpMore);
ibInfo.setOnClickListener(new View.OnClickListener() {
@Override
@ -374,11 +412,9 @@ public class FragmentDialogSearch extends FragmentDialogBase {
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
BoundaryCallbackMessages.SearchCriteria criteria = new BoundaryCallbackMessages.SearchCriteria();
BoundaryCallbackMessages.SearchCriteria criteria = getSearchCriteria();
criteria.query = etQuery.getText().toString().trim();
if (!TextUtils.isEmpty(criteria.query)) {
if (criteria.query != null) {
List<String> searches = new ArrayList<>();
for (int i = 1; i <= 3; i++)
if (prefs.contains("last_search" + i)) {
@ -395,43 +431,6 @@ public class FragmentDialogSearch extends FragmentDialogBase {
editor.apply();
}
if (TextUtils.isEmpty(criteria.query))
criteria.query = null;
criteria.fts = cbSearchIndex.isChecked();
if (!criteria.fts) {
criteria.in_senders = cbSenders.isChecked();
criteria.in_recipients = cbRecipients.isChecked();
criteria.in_subject = cbSubject.isChecked();
criteria.in_keywords = cbKeywords.isChecked();
criteria.in_message = cbMessage.isChecked();
criteria.in_notes = cbNotes.isChecked();
criteria.in_headers = cbHeaders.isChecked();
criteria.in_html = cbHtml.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();
int pos = spMessageSize.getSelectedItemPosition();
if (pos > 0) {
int[] sizes = getResources().getIntArray(R.array.sizeValues);
criteria.with_size = sizes[pos];
}
}
criteria.in_trash = cbSearchTrash.isChecked();
criteria.in_junk = cbSearchJunk.isChecked();
Object after = tvAfter.getTag();
Object before = tvBefore.getTag();
if (after != null)
criteria.after = ((Calendar) after).getTimeInMillis();
if (before != null)
criteria.before = ((Calendar) before).getTimeInMillis();
Helper.hideKeyboard(etQuery);
if (criteria.query != null && criteria.query.startsWith("raw:"))
@ -482,6 +481,38 @@ public class FragmentDialogSearch extends FragmentDialogBase {
account, folder, false, criteria);
}
})
.setNeutralButton(R.string.title_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
BoundaryCallbackMessages.SearchCriteria criteria = getSearchCriteria();
Bundle args = new Bundle();
args.putString("name", criteria.getTitle(context));
args.putString("data", criteria.toJson().toString());
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) {
EntitySearch search = new EntitySearch();
search.name = args.getString("name");
search.data = args.getString("data");
DB db = DB.getInstance(context);
db.search().insertSearch(search);
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}.execute(FragmentDialogSearch.this, args, "search:save");
} catch (Throwable ex) {
Log.e(ex);
}
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@ -547,6 +578,50 @@ public class FragmentDialogSearch extends FragmentDialogBase {
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
}
private BoundaryCallbackMessages.SearchCriteria getSearchCriteria() {
BoundaryCallbackMessages.SearchCriteria criteria = new BoundaryCallbackMessages.SearchCriteria();
criteria.query = etQuery.getText().toString().trim();
if (TextUtils.isEmpty(criteria.query))
criteria.query = null;
criteria.fts = cbSearchIndex.isChecked();
if (!criteria.fts) {
criteria.in_senders = cbSenders.isChecked();
criteria.in_recipients = cbRecipients.isChecked();
criteria.in_subject = cbSubject.isChecked();
criteria.in_keywords = cbKeywords.isChecked();
criteria.in_message = cbMessage.isChecked();
criteria.in_notes = cbNotes.isChecked();
criteria.in_headers = cbHeaders.isChecked();
criteria.in_html = cbHtml.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();
int pos = spMessageSize.getSelectedItemPosition();
if (pos > 0) {
int[] sizes = getResources().getIntArray(R.array.sizeValues);
criteria.with_size = sizes[pos];
}
}
criteria.in_trash = cbSearchTrash.isChecked();
criteria.in_junk = cbSearchJunk.isChecked();
Object after = tvAfter.getTag();
Object before = tvBefore.getTag();
if (after != null)
criteria.after = ((Calendar) after).getTimeInMillis();
if (before != null)
criteria.before = ((Calendar) before).getTimeInMillis();
return criteria;
}
private void pickDate(TextView tv) {
Object tag = tv.getTag();
final Calendar cal = (tag == null ? Calendar.getInstance() : (Calendar) tag);

View File

@ -159,7 +159,7 @@
app:layout_constraintTop_toBottomOf="@id/rvUnified" />
<ImageButton
android:id="@+id/ibExpanderMenu"
android:id="@+id/ibExpanderSearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
@ -169,6 +169,36 @@
app:layout_constraintTop_toBottomOf="@id/vSeparatorUnified"
app:srcCompat="@drawable/expander" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:animateLayoutChanges="false"
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ibExpanderSearch" />
<View
android:id="@+id/vSeparatorSearch"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?attr/colorSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rvSearch" />
<ImageButton
android:id="@+id/ibExpanderMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/title_legend_expander"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorSearch"
app:srcCompat="@drawable/expander" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMenu"
android:layout_width="0dp"

View File

@ -6,7 +6,7 @@
### Next version
* Storing folder namespaces
* Added saved searches
### 1.1732