Added rule UI

pull/147/head
M66B 6 years ago
parent 3113d71ced
commit 94c5c3aa23

@ -38,7 +38,6 @@ Anything on this list is in random order and *might* be added in the near future
* Swipe left/right to go to previous/next message: besides that swiping left/right is already being used to move messages to archive/trash, swiping also selects message text, so this will not work reliably. You can use the bottom navigation bar instead.
* Rich text editor: besides that very few people would use this on a small mobile device, Android doesn't support a rich text editor and most rich text editor open source projects are abandoned.
* Widget to read e-mail: widgets can have limited user interaction only, so a widget to read e-mail would not be very useful. Moreover, it would be not very useful to duplicate functions which are already available in the app.
* Executing filter rules: filter rules should be executed on the server because a battery powered device with possibly an unstable internet connection is not suitable for this.
* Badge count: there is no standard Android API for this and third party solutions might stop working anytime. For example *ShortcutBadger* [has lots of problems](https://github.com/leolin310148/ShortcutBadger/issues). You can use the provided widget instead.
* Switch language: Android is not designed to change the language of an app and on recent Android versions it even causes problems. So, better fix the translation in your language if needed, see [this FAQ](#user-content-faq26) about how to.
* Select identities to show in unified inbox: this would add complexity for something which would hardly be used.

File diff suppressed because it is too large Load Diff

@ -130,6 +130,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
static final String ACTION_STORE_RAW = BuildConfig.APPLICATION_ID + ".STORE_RAW";
static final String ACTION_EDIT_FOLDER = BuildConfig.APPLICATION_ID + ".EDIT_FOLDER";
static final String ACTION_EDIT_ANSWER = BuildConfig.APPLICATION_ID + ".EDIT_ANSWER";
static final String ACTION_EDIT_RULE = BuildConfig.APPLICATION_ID + ".EDIT_RULE";
static final String ACTION_STORE_ATTACHMENT = BuildConfig.APPLICATION_ID + ".STORE_ATTACHMENT";
static final String ACTION_STORE_ATTACHMENTS = BuildConfig.APPLICATION_ID + ".STORE_ATTACHMENTS";
static final String ACTION_DECRYPT = BuildConfig.APPLICATION_ID + ".DECRYPT";
@ -183,6 +184,9 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
case R.string.menu_answers:
onMenuAnswers();
break;
case R.string.menu_rules:
onMenuRules();
break;
case R.string.menu_operations:
onMenuOperations();
break;
@ -300,6 +304,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
drawerArray.add(new DrawerItem(R.layout.item_drawer_separator));
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_reply_24, R.string.menu_answers));
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_filter_list_24, R.string.menu_rules));
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_list_24, R.string.menu_operations));
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_settings_applications_24, R.string.menu_setup));
@ -474,6 +479,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
iff.addAction(ACTION_STORE_RAW);
iff.addAction(ACTION_EDIT_FOLDER);
iff.addAction(ACTION_EDIT_ANSWER);
iff.addAction(ACTION_EDIT_RULE);
iff.addAction(ACTION_STORE_ATTACHMENT);
iff.addAction(ACTION_STORE_ATTACHMENTS);
iff.addAction(ACTION_DECRYPT);
@ -871,6 +877,12 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
fragmentTransaction.commit();
}
private void onMenuRules() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentRules()).addToBackStack("rules");
fragmentTransaction.commit();
}
private void onMenuOperations() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentOperations()).addToBackStack("operations");
@ -1009,6 +1021,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
onEditFolder(intent);
else if (ACTION_EDIT_ANSWER.equals(action))
onEditAnswer(intent);
else if (ACTION_EDIT_RULE.equals(action))
onEditRule(intent);
else if (ACTION_STORE_ATTACHMENT.equals(action))
onStoreAttachment(intent);
else if (ACTION_STORE_ATTACHMENTS.equals(action))
@ -1101,6 +1115,14 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
fragmentTransaction.commit();
}
private void onEditRule(Intent intent) {
FragmentRule fragment = new FragmentRule();
fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("rule");
fragmentTransaction.commit();
}
private void onStoreAttachment(Intent intent) {
attachment = intent.getLongExtra("id", -1);
Intent create = new Intent(Intent.ACTION_CREATE_DOCUMENT);

@ -0,0 +1,197 @@
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-2019 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
private Context context;
private LifecycleOwner owner;
private LayoutInflater inflater;
private List<EntityRule> all = new ArrayList<>();
private List<EntityRule> filtered = new ArrayList<>();
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private View itemView;
private TextView tvName;
ViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
tvName = itemView.findViewById(R.id.tvName);
}
private void wire() {
itemView.setOnClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
}
private void bindTo(EntityRule rule) {
tvName.setText(rule.name);
}
@Override
public void onClick(View v) {
int pos = getAdapterPosition();
if (pos == RecyclerView.NO_POSITION)
return;
EntityRule rule = filtered.get(pos);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_EDIT_RULE)
.putExtra("id", rule.id));
}
}
AdapterRule(Context context, LifecycleOwner owner) {
this.context = context;
this.owner = owner;
this.inflater = LayoutInflater.from(context);
setHasStableIds(true);
}
public void set(@NonNull List<EntityRule> rules) {
Log.i("Set rules=" + rules.size());
final Collator collator = Collator.getInstance(Locale.getDefault());
collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc
Collections.sort(rules, new Comparator<EntityRule>() {
@Override
public int compare(EntityRule r1, EntityRule r2) {
return collator.compare(r1.name, r2.name);
}
});
all = rules;
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new MessageDiffCallback(filtered, all));
filtered.clear();
filtered.addAll(all);
diff.dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
Log.i("Inserted @" + position + " #" + count);
}
@Override
public void onRemoved(int position, int count) {
Log.i("Removed @" + position + " #" + count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
Log.i("Moved " + fromPosition + ">" + toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
Log.i("Changed @" + position + " #" + count);
}
});
diff.dispatchUpdatesTo(this);
}
private class MessageDiffCallback extends DiffUtil.Callback {
private List<EntityRule> prev;
private List<EntityRule> next;
MessageDiffCallback(List<EntityRule> prev, List<EntityRule> next) {
this.prev = prev;
this.next = next;
}
@Override
public int getOldListSize() {
return prev.size();
}
@Override
public int getNewListSize() {
return next.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
EntityRule r1 = prev.get(oldItemPosition);
EntityRule r2 = next.get(newItemPosition);
return r1.id.equals(r2.id);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntityRule r1 = prev.get(oldItemPosition);
EntityRule r2 = next.get(newItemPosition);
return r1.equals(r2);
}
}
@Override
public long getItemId(int position) {
return filtered.get(position).id;
}
@Override
public int getItemCount() {
return filtered.size();
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(inflater.inflate(R.layout.item_rule, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire();
EntityRule rule = filtered.get(position);
holder.bindTo(rule);
holder.wire();
}
}

@ -49,7 +49,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 36,
version = 37,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -58,6 +58,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
EntityAttachment.class,
EntityOperation.class,
EntityAnswer.class,
EntityRule.class,
EntityLog.class
}
)
@ -78,6 +79,8 @@ public abstract class DB extends RoomDatabase {
public abstract DaoAnswer answer();
public abstract DaoRule rule();
public abstract DaoLog log();
private static DB sInstance;
@ -433,6 +436,23 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `message` ADD COLUMN `warning` TEXT");
}
})
.addMigrations(new Migration(36, 37) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("CREATE TABLE `rule`" +
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT," +
" `folder` INTEGER NOT NULL," +
" `name` TEXT NOT NULL," +
" `order` INTEGER NOT NULL," +
" `condition` TEXT NOT NULL," +
" `action` TEXT NOT NULL," +
" `enabled` INTEGER NOT NULL," +
" FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)");
db.execSQL("CREATE INDEX `index_rule_folder` ON `rule` (`folder`)");
db.execSQL("CREATE INDEX `index_rule_order` ON `rule` (`order`)");
}
})
.build();
}

@ -0,0 +1,51 @@
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-2019 by Marcel Bokhorst (M66B)
*/
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
@Dao
public interface DaoRule {
@Query("SELECT * FROM rule WHERE folder = :folder")
List<EntityRule> getRules(long folder);
@Query("SELECT rule.*, folder.account FROM rule" +
" JOIN folder ON folder.id = rule.folder" +
" WHERE rule.id = :id")
TupleRuleEx getRule(long id);
@Query("SELECT * FROM rule")
LiveData<List<EntityRule>> liveRules();
@Insert
long insertRule(EntityRule rule);
@Update
int updateRule(EntityRule rule);
@Query("DELETE FROM rule WHERE id = :id")
void deleteRule(long id);
}

@ -0,0 +1,144 @@
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-2019 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import static androidx.room.ForeignKey.CASCADE;
@Entity(
tableName = EntityRule.TABLE_NAME,
foreignKeys = {
@ForeignKey(childColumns = "folder", entity = EntityFolder.class, parentColumns = "id", onDelete = CASCADE),
},
indices = {
@Index(value = {"folder"}),
@Index(value = {"order"})
}
)
public class EntityRule {
static final String TABLE_NAME = "rule";
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull
public Long folder;
@NonNull
public String name;
@NonNull
public int order;
@NonNull
public String condition;
@NonNull
public String action;
@NonNull
public boolean enabled;
static final int TYPE_SEEN = 1;
static final int TYPE_UNSEEN = 2;
static final int TYPE_MOVE = 3;
boolean matches(Context context, EntityMessage message) throws JSONException, IOException {
JSONObject jcondition = new JSONObject(condition);
String sender = jcondition.getString("sender");
String subject = jcondition.getString("subject");
String text = jcondition.getString("text");
boolean regex = jcondition.getBoolean("regex");
if (sender != null && message.from != null) {
if (matches(sender, MessageHelper.getFormattedAddresses(message.from, true), regex))
return true;
}
if (subject != null && message.subject != null) {
if (matches(subject, message.subject, regex))
return true;
}
if (text != null && message.content) {
String body = message.read(context);
String santized = HtmlHelper.sanitize(body, true);
if (matches(text, santized, regex))
return true;
}
return false;
}
private boolean matches(String needle, String haystack, boolean regex) {
if (regex) {
Pattern pattern = Pattern.compile(needle);
return pattern.matcher(haystack).matches();
} else
return haystack.contains(needle);
}
void execute(Context context, DB db, EntityMessage message) throws JSONException {
JSONObject jargs = new JSONObject(action);
switch (jargs.getInt("type")) {
case TYPE_SEEN:
onActionSeen(context, db, message, true);
break;
case TYPE_UNSEEN:
onActionSeen(context, db, message, false);
break;
case TYPE_MOVE:
onActionMove(context, db, message, jargs);
break;
}
}
private void onActionSeen(Context context, DB db, EntityMessage message, boolean seen) {
EntityOperation.queue(context, db, message, EntityOperation.SEEN, seen);
}
private void onActionMove(Context context, DB db, EntityMessage message, JSONObject jargs) throws JSONException {
long target = jargs.getLong("target");
boolean seen = jargs.getBoolean("seen");
if (seen)
EntityOperation.queue(context, db, message, EntityOperation.SEEN, true);
EntityOperation.queue(context, db, message, EntityOperation.MOVE, target);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityRule) {
EntityRule other = (EntityRule) obj;
return this.folder.equals(other.folder) &&
this.name.equals(other.name) &&
this.condition.equals(other.condition) &&
this.action.equals(other.action) &&
this.enabled == other.enabled;
} else
return false;
}
}

@ -0,0 +1,471 @@
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-2019 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.snackbar.Snackbar;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import androidx.lifecycle.Lifecycle;
public class FragmentRule extends FragmentBase {
private ViewGroup view;
private EditText etName;
private Spinner spAccount;
private Spinner spFolder;
private EditText etOrder;
private EditText etSender;
private EditText etSubject;
private EditText etText;
private Spinner spAction;
private Spinner spMove;
private CheckBox cbMoveSeen;
private BottomNavigationView bottom_navigation;
private ContentLoadingProgressBar pbWait;
private Group grpReady;
private Group grpMove;
private ArrayAdapter<EntityAccount> adapterAccount;
private ArrayAdapter<EntityFolder> adapterFolder;
private ArrayAdapter<Action> adapterAction;
private long id = -1;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get arguments
Bundle args = getArguments();
id = (args == null ? -1 : args.getLong("id", -1));
}
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = (ViewGroup) inflater.inflate(R.layout.fragment_rule, container, false);
// Get controls
etName = view.findViewById(R.id.etName);
spAccount = view.findViewById(R.id.spAccount);
spFolder = view.findViewById(R.id.spFolder);
etOrder = view.findViewById(R.id.etOrder);
etSender = view.findViewById(R.id.etSender);
etSubject = view.findViewById(R.id.etSubject);
etText = view.findViewById(R.id.etText);
spAction = view.findViewById(R.id.spAction);
spMove = view.findViewById(R.id.spMove);
cbMoveSeen = view.findViewById(R.id.cbMoveSeen);
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
grpMove = view.findViewById(R.id.grpMove);
adapterAccount = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList<EntityAccount>());
adapterAccount.setDropDownViewResource(R.layout.spinner_item1_dropdown);
spAccount.setAdapter(adapterAccount);
adapterFolder = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList<EntityFolder>());
adapterFolder.setDropDownViewResource(R.layout.spinner_item1_dropdown);
spFolder.setAdapter(adapterFolder);
adapterAction = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList<Action>());
adapterAction.setDropDownViewResource(R.layout.spinner_item1_dropdown);
spAction.setAdapter(adapterAction);
List<Action> actions = new ArrayList<>();
actions.add(new Action(EntityRule.TYPE_SEEN, getString(R.string.title_seen)));
actions.add(new Action(EntityRule.TYPE_UNSEEN, getString(R.string.title_unseen)));
//actions.add(new Action(EntityRule.TYPE_MOVE, getString(R.string.title_move)));
adapterAction.addAll(actions);
spAccount.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
EntityAccount account = (EntityAccount) adapterView.getAdapter().getItem(position);
onAccountSelected(account.id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
onAccountSelected(-1);
}
});
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.action_delete:
onActionTrash();
return true;
case R.id.action_save:
onActionSave();
return true;
default:
return false;
}
}
});
((ActivityBase) getActivity()).addBackPressedListener(onBackPressedListener);
// Initialize
bottom_navigation.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
grpMove.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
return view;
}
@Override
public void onDestroyView() {
((ActivityBase) getActivity()).removeBackPressedListener(onBackPressedListener);
super.onDestroyView();
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
new SimpleTask<List<EntityAccount>>() {
@Override
protected List<EntityAccount> onExecute(Context context, Bundle args) {
return DB.getInstance(context).account().getAccounts(true);
}
@Override
protected void onExecuted(Bundle args, List<EntityAccount> accounts) {
if (accounts == null)
accounts = new ArrayList<>();
adapterAccount.addAll(accounts);
Bundle rargs = new Bundle();
rargs.putLong("id", id);
new SimpleTask<TupleRuleEx>() {
@Override
protected TupleRuleEx onExecute(Context context, Bundle args) {
long id = args.getLong("id");
return DB.getInstance(context).rule().getRule(id);
}
@Override
protected void onExecuted(Bundle args, TupleRuleEx rule) {
try {
JSONObject jcondition = (rule == null ? new JSONObject() : new JSONObject(rule.condition));
JSONObject jaction = (rule == null ? new JSONObject() : new JSONObject(rule.action));
etName.setText(rule == null ? null : rule.name);
etOrder.setText(rule == null ? null : Integer.toString(rule.order));
etSender.setText(jcondition.optString("sender"));
etSubject.setText(jcondition.optString("subject"));
etText.setText(jcondition.optString("text"));
int type = jaction.optInt("type", -1);
for (int pos = 0; pos < adapterAction.getCount(); pos++)
if (adapterAction.getItem(pos).type == type) {
spAction.setSelection(pos);
break;
}
bottom_navigation.findViewById(R.id.action_delete).setVisibility(rule == null ? View.GONE : View.VISIBLE);
if (rule == null) {
grpReady.setVisibility(View.VISIBLE);
bottom_navigation.setVisibility(View.VISIBLE);
pbWait.setVisibility(View.GONE);
} else {
spAccount.setTag(rule.account);
spFolder.setTag(rule.folder);
for (int pos = 0; pos < adapterAccount.getCount(); pos++)
if (adapterAccount.getItem(pos).id.equals(rule.account)) {
spAccount.setSelection(pos);
onAccountSelected(rule.account);
break;
}
}
} catch (JSONException ex) {
Log.e(ex);
}
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentRule.this, rargs, "rule:get");
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(this, null, "rule:accounts");
}
private void onAccountSelected(long account) {
Bundle args = new Bundle();
args.putLong("account", account);
new SimpleTask<List<EntityFolder>>() {
@Override
protected List<EntityFolder> onExecute(Context context, Bundle args) {
long account = args.getLong("account");
return DB.getInstance(context).folder().getFolders(account);
}
@Override
protected void onExecuted(Bundle args, List<EntityFolder> folders) {
adapterFolder.clear();
adapterFolder.addAll(folders);
long account = args.getLong("account");
if (account == (Long) spAccount.getTag()) {
Long folder = (Long) spFolder.getTag();
for (int pos = 0; pos < folders.size(); pos++)
if (folders.get(pos).id.equals(folder)) {
spFolder.setSelection(pos);
break;
}
} else
spFolder.setSelection(0);
grpReady.setVisibility(View.VISIBLE);
bottom_navigation.setVisibility(View.VISIBLE);
pbWait.setVisibility(View.GONE);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentRule.this, args, "rule:folders");
}
private void onActionTrash() {
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
.setMessage(R.string.title_ask_delete_rule)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Bundle args = new Bundle();
args.putLong("id", id);
new SimpleTask<Void>() {
@Override
protected void onPreExecute(Bundle args) {
Helper.setViewsEnabled(view, false);
}
@Override
protected void onPostExecute(Bundle args) {
Helper.setViewsEnabled(view, true);
}
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
DB.getInstance(context).rule().deleteRule(id);
return null;
}
@Override
protected void onExecuted(Bundle args, Void data) {
finish();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentRule.this, args, "rule:delete");
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void onActionSave() {
try {
Helper.setViewsEnabled(view, false);
EntityFolder folder = (EntityFolder) spFolder.getSelectedItem();
String sender = etSender.getText().toString();
String subject = etSubject.getText().toString();
String text = etText.getText().toString();
JSONObject jcondition = new JSONObject();
if (!TextUtils.isEmpty(sender))
jcondition.put("sender", sender);
if (!TextUtils.isEmpty(subject))
jcondition.put("subject", subject);
if (!TextUtils.isEmpty(text))
jcondition.put("text", text);
Action action = (Action) spAction.getSelectedItem();
JSONObject jaction = new JSONObject();
if (action != null)
jaction.put("type", action.type);
Bundle args = new Bundle();
args.putLong("id", id);
args.putLong("folder", folder == null ? -1 : folder.id);
args.putString("name", etName.getText().toString());
args.putString("order", etOrder.getText().toString());
args.putString("condition", jcondition.toString());
args.putString("action", jaction.toString());
new SimpleTask<Void>() {
@Override
protected void onPreExecute(Bundle args) {
Helper.setViewsEnabled(view, false);
}
@Override
protected void onPostExecute(Bundle args) {
Helper.setViewsEnabled(view, true);
}
@Override
protected Void onExecute(Context context, Bundle args) {
long id = args.getLong("id");
long folder = args.getLong("folder");
String name = args.getString("name");
String order = args.getString("order");
String condition = args.getString("condition");
String action = args.getString("action");
if (TextUtils.isEmpty(name))
throw new IllegalArgumentException(getString(R.string.title_rule_name_missing));
if (TextUtils.isEmpty(order))
order = "1";
DB db = DB.getInstance(context);
if (id < 0) {
EntityRule rule = new EntityRule();
rule.folder = folder;
rule.name = name;
rule.order = Integer.parseInt(order);
rule.condition = condition;
rule.action = action;
rule.id = db.rule().insertRule(rule);
} else {
EntityRule rule = db.rule().getRule(id);
rule.folder = folder;
rule.name = name;
rule.order = Integer.parseInt(order);
rule.condition = condition;
rule.action = action;
db.rule().updateRule(rule);
}
return null;
}
@Override
protected void onExecuted(Bundle args, Void data) {
finish();
}
@Override
protected void onException(Bundle args, Throwable ex) {
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(this, args, "rule:save");
} catch (JSONException ex) {
Log.e(ex);
}
}
private void handleExit() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
.setMessage(R.string.title_ask_save)
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onActionSave();
}
})
.setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
ActivityBase.IBackPressedListener onBackPressedListener = new ActivityBase.IBackPressedListener() {
@Override
public boolean onBackPressed() {
handleExit();
return true;
}
};
private class Action {
int type;
String name;
Action(int type, String name) {
this.type = type;
this.name = name;
}
@NonNull
@Override
public String toString() {
return name;
}
}
}

@ -0,0 +1,104 @@
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-2019 by Marcel Bokhorst (M66B)
*/
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class FragmentRules extends FragmentBase {
private RecyclerView rvRule;
private ContentLoadingProgressBar pbWait;
private Group grpReady;
private FloatingActionButton fab;
private AdapterRule adapter;
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
setSubtitle(R.string.menu_rules);
View view = inflater.inflate(R.layout.fragment_rules, container, false);
// Get controls
rvRule = view.findViewById(R.id.rvRule);
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab);
// Wire controls
rvRule.setHasFixedSize(false);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvRule.setLayoutManager(llm);
adapter = new AdapterRule(getContext(), getViewLifecycleOwner());
rvRule.setAdapter(adapter);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentRule()).addToBackStack("rule");
fragmentTransaction.commit();
}
});
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
DB db = DB.getInstance(getContext());
db.rule().liveRules().observe(getViewLifecycleOwner(), new Observer<List<EntityRule>>() {
@Override
public void onChanged(List<EntityRule> rules) {
if (rules == null)
rules = new ArrayList<>();
adapter.set(rules);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
}
});
}
}

@ -0,0 +1,34 @@
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-2019 by Marcel Bokhorst (M66B)
*/
public class TupleRuleEx extends EntityRule {
public long account;
@Override
public boolean equals(Object obj) {
if (obj instanceof TupleRuleEx) {
TupleRuleEx other = (TupleRuleEx) obj;
return (super.equals(obj) &&
this.account == other.account);
} else
return false;
}
}

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
</vector>

@ -0,0 +1,255 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ActivityView">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="12dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_rule_name"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/etName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<!-- account -->
<TextView
android:id="@+id/tvAccount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_account"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
<Spinner
android:id="@+id/spAccount"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccount" />
<!-- folder -->
<TextView
android:id="@+id/tvFolder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_folder"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spAccount" />
<Spinner
android:id="@+id/spFolder"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvFolder" />
<TextView
android:id="@+id/tvFolderRemark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_rule_folder_remark"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spFolder" />
<TextView
android:id="@+id/tvOrder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_order"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvFolderRemark" />
<EditText
android:id="@+id/etOrder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="1"
android:inputType="number"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvOrder" />
<!-- condition -->
<TextView
android:id="@+id/tvSender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_sender"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etOrder" />
<EditText
android:id="@+id/etSender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSender" />
<TextView
android:id="@+id/tvSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_subject"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etSender" />
<EditText
android:id="@+id/etSubject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSubject" />
<TextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_text"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etSubject" />
<EditText
android:id="@+id/etText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvText" />
<TextView
android:id="@+id/tvAction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_action"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etText" />
<Spinner
android:id="@+id/spAction"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAction" />
<TextView
android:id="@+id/tvMove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_folder"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spAction" />
<Spinner
android:id="@+id/spMove"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvMove" />
<CheckBox
android:id="@+id/cbMoveSeen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_rule_seen"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spMove" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpReady"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvName,etName,tvAccount,spAccount,tvFolder,spFolder,tvFolderRemark,tvOrder,etOrder,spFolder,tvSender,etSender,tvSubject,etSubject,tvText,etText,tvAction,spAction" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpMove"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvMove,spMove,cbMoveSeen" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorPrimary"
app:itemIconTint="@color/bottomnav_foreground"
app:itemTextColor="@color/bottomnav_foreground"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/action_rule" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ActivityView">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRule"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_padding"
android:src="@drawable/baseline_add_24"
android:tint="@color/colorActionForeground"
android:tooltipText="@string/title_add"
app:backgroundTint="?attr/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpReady"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="rvRule" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Name"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/vSeparator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_delete"
android:icon="@drawable/baseline_delete_24"
android:title="@string/title_delete" />
<item
android:id="@+id/action_save"
android:icon="@drawable/baseline_save_alt_24"
android:title="@string/title_save" />
</menu>

@ -50,6 +50,7 @@
<string name="menu_setup">Setup</string>
<string name="menu_answers">Templates</string>
<string name="menu_rules">Rules</string>
<string name="menu_operations">Operations</string>
<string name="menu_legend">Legend</string>
<string name="menu_faq">Support</string>
@ -305,6 +306,7 @@
<string name="title_ask_delete">Delete message permanently?</string>
<string name="title_ask_delete_selected">Delete selected messages permanently?</string>
<string name="title_ask_delete_answer">Delete reply template permanently?</string>
<string name="title_ask_delete_rule">Delete rule permanently?</string>
<string name="title_ask_discard">Discard draft?</string>
<string name="title_ask_save">Save changes?</string>
<string name="title_ask_spam">Report as spam?</string>
@ -379,6 +381,19 @@
<string name="title_answer_template_name">$name$ will be replaced by the sender full name</string>
<string name="title_answer_template_email">$email$ will be replaced by the sender email address</string>
<string name="title_rule_name">Name</string>
<string name="title_rule_account">Account</string>
<string name="title_rule_folder">Folder</string>
<string name="title_rule_folder_remark">The texts of messages in the selected folder will always be downloaded</string>
<string name="title_rule_order">Order</string>
<string name="title_rule_sender">Sender</string>
<string name="title_rule_subject">Subject</string>
<string name="title_rule_text">Text</string>
<string name="title_rule_action">Action</string>
<string name="title_rule_target">Target</string>
<string name="title_rule_seen">Mark as read</string>
<string name="title_rule_name_missing">Rule name missing</string>
<string name="title_action_seen">Mark read</string>
<string name="title_action_archive">Archive</string>
<string name="title_action_trash">Trash</string>

Loading…
Cancel
Save