diff --git a/app/src/main/java/eu/faircode/email/AdapterRule.java b/app/src/main/java/eu/faircode/email/AdapterRule.java index 0f2fe1fb3c..7c4f0c75f6 100644 --- a/app/src/main/java/eu/faircode/email/AdapterRule.java +++ b/app/src/main/java/eu/faircode/email/AdapterRule.java @@ -45,18 +45,20 @@ public class AdapterRule extends RecyclerView.Adapter { private LifecycleOwner owner; private LayoutInflater inflater; - private List all = new ArrayList<>(); - private List filtered = new ArrayList<>(); + private List all = new ArrayList<>(); + private List filtered = new ArrayList<>(); public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private View itemView; private TextView tvName; + private TextView tvFolder; ViewHolder(View itemView) { super(itemView); this.itemView = itemView.findViewById(R.id.clItem); tvName = itemView.findViewById(R.id.tvName); + tvFolder = itemView.findViewById(R.id.tvFolder); } private void wire() { @@ -67,9 +69,10 @@ public class AdapterRule extends RecyclerView.Adapter { itemView.setOnClickListener(null); } - private void bindTo(EntityRule rule) { + private void bindTo(TupleRuleEx rule) { itemView.setActivated(!rule.enabled); tvName.setText(rule.name); + tvFolder.setText(rule.folderName + "/" + rule.accountName); } @Override @@ -78,7 +81,7 @@ public class AdapterRule extends RecyclerView.Adapter { if (pos == RecyclerView.NO_POSITION) return; - EntityRule rule = filtered.get(pos); + TupleRuleEx rule = filtered.get(pos); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context); lbm.sendBroadcast( @@ -94,15 +97,24 @@ public class AdapterRule extends RecyclerView.Adapter { setHasStableIds(true); } - public void set(@NonNull List rules) { + public void set(@NonNull List 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() { + Collections.sort(rules, new Comparator() { @Override - public int compare(EntityRule r1, EntityRule r2) { + public int compare(TupleRuleEx r1, TupleRuleEx r2) { + int a = collator.compare(r1.accountName, r2.accountName); + if (a != 0) + return a; + int f = collator.compare(r1.folderName, r2.folderName); + if (f != 0) + return f; + int o = ((Integer) r1.order).compareTo(r2.order); + if (o != 0) + return 0; return collator.compare(r1.name, r2.name); } }); @@ -139,10 +151,10 @@ public class AdapterRule extends RecyclerView.Adapter { } private class MessageDiffCallback extends DiffUtil.Callback { - private List prev; - private List next; + private List prev; + private List next; - MessageDiffCallback(List prev, List next) { + MessageDiffCallback(List prev, List next) { this.prev = prev; this.next = next; } @@ -159,15 +171,15 @@ public class AdapterRule extends RecyclerView.Adapter { @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - EntityRule r1 = prev.get(oldItemPosition); - EntityRule r2 = next.get(newItemPosition); + TupleRuleEx r1 = prev.get(oldItemPosition); + TupleRuleEx 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); + TupleRuleEx r1 = prev.get(oldItemPosition); + TupleRuleEx r2 = next.get(newItemPosition); return r1.equals(r2); } } @@ -191,7 +203,7 @@ public class AdapterRule extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.unwire(); - EntityRule rule = filtered.get(position); + TupleRuleEx rule = filtered.get(position); holder.bindTo(rule); holder.wire(); } diff --git a/app/src/main/java/eu/faircode/email/DaoRule.java b/app/src/main/java/eu/faircode/email/DaoRule.java index 89f3ec6abd..98ddad1067 100644 --- a/app/src/main/java/eu/faircode/email/DaoRule.java +++ b/app/src/main/java/eu/faircode/email/DaoRule.java @@ -31,16 +31,20 @@ import androidx.room.Update; public interface DaoRule { @Query("SELECT * FROM rule" + " WHERE folder = :folder" + + " AND enabled" + " ORDER BY `order`") - List getRules(long folder); + List getEnabledRules(long folder); - @Query("SELECT rule.*, folder.account FROM rule" + + @Query("SELECT rule.*, folder.account, folder.name AS folderName, account.name AS accountName FROM rule" + " JOIN folder ON folder.id = rule.folder" + + " JOIN account ON account.id = folder.account" + " WHERE rule.id = :id") TupleRuleEx getRule(long id); - @Query("SELECT * FROM rule ORDER BY `order`") - LiveData> liveRules(); + @Query("SELECT rule.*, folder.account, folder.name AS folderName, account.name AS accountName FROM rule" + + " JOIN folder ON folder.id = rule.folder" + + " JOIN account ON account.id = folder.account") + LiveData> liveRules(); @Insert long insertRule(EntityRule rule); diff --git a/app/src/main/java/eu/faircode/email/EntityRule.java b/app/src/main/java/eu/faircode/email/EntityRule.java index c745bcbf3f..d6e4155d3c 100644 --- a/app/src/main/java/eu/faircode/email/EntityRule.java +++ b/app/src/main/java/eu/faircode/email/EntityRule.java @@ -67,28 +67,32 @@ public class EntityRule { 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"); + boolean matches(Context context, EntityMessage message) throws IOException { + try { + JSONObject jcondition = new JSONObject(condition); + String sender = jcondition.optString("sender", null); + String subject = jcondition.optString("subject", null); + String text = jcondition.optString("text", null); + boolean regex = jcondition.optBoolean("regex", false); - if (sender != null && message.from != null) { - if (matches(sender, MessageHelper.getFormattedAddresses(message.from, true), regex)) - return true; - } + 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 (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; + if (text != null && message.content) { + String body = message.read(context); + String santized = HtmlHelper.sanitize(body, true); + if (matches(text, santized, regex)) + return true; + } + } catch (JSONException ex) { + Log.e(ex); } return false; @@ -102,30 +106,35 @@ public class EntityRule { 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; + void execute(Context context, DB db, EntityMessage message) { + try { + JSONObject jargs = new JSONObject(action); + int type = jargs.getInt("type"); + Log.i("Executing rule=" + type + " message=" + message.id); + + switch (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; + } + } catch (JSONException ex) { + Log.e(ex); } } private void onActionSeen(Context context, DB db, EntityMessage message, boolean seen) { EntityOperation.queue(context, db, message, EntityOperation.SEEN, seen); + message.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); } diff --git a/app/src/main/java/eu/faircode/email/FragmentRule.java b/app/src/main/java/eu/faircode/email/FragmentRule.java index d07612a403..519412cf7c 100644 --- a/app/src/main/java/eu/faircode/email/FragmentRule.java +++ b/app/src/main/java/eu/faircode/email/FragmentRule.java @@ -58,8 +58,7 @@ public class FragmentRule extends FragmentBase { private EditText etSubject; private EditText etText; private Spinner spAction; - private Spinner spMove; - private CheckBox cbMoveSeen; + private Spinner spTarget; private BottomNavigationView bottom_navigation; private ContentLoadingProgressBar pbWait; private Group grpReady; @@ -68,6 +67,7 @@ public class FragmentRule extends FragmentBase { private ArrayAdapter adapterAccount; private ArrayAdapter adapterFolder; private ArrayAdapter adapterAction; + private ArrayAdapter adapterTarget; private long id = -1; @@ -95,8 +95,7 @@ public class FragmentRule extends FragmentBase { 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); + spTarget = view.findViewById(R.id.spTarget); bottom_navigation = view.findViewById(R.id.bottom_navigation); pbWait = view.findViewById(R.id.pbWait); grpReady = view.findViewById(R.id.grpReady); @@ -114,10 +113,14 @@ public class FragmentRule extends FragmentBase { adapterAction.setDropDownViewResource(R.layout.spinner_item1_dropdown); spAction.setAdapter(adapterAction); + adapterTarget = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList()); + adapterTarget.setDropDownViewResource(R.layout.spinner_item1_dropdown); + spTarget.setAdapter(adapterTarget); + List 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))); + actions.add(new Action(EntityRule.TYPE_MOVE, getString(R.string.title_move))); adapterAction.addAll(actions); spAccount.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -133,6 +136,23 @@ public class FragmentRule extends FragmentBase { } }); + spAction.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, long id) { + Action action = (Action) adapterView.getAdapter().getItem(position); + onActionSelected(action.type); + } + + @Override + public void onNothingSelected(AdapterView parent) { + onActionSelected(-1); + } + + private void onActionSelected(int type) { + grpMove.setVisibility(type == EntityRule.TYPE_MOVE ? View.VISIBLE : View.GONE); + } + }); + bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { @@ -223,6 +243,9 @@ public class FragmentRule extends FragmentBase { spAccount.setTag(rule.account); spFolder.setTag(rule.folder); + if (type == EntityRule.TYPE_MOVE) + spTarget.setTag(jaction.getLong("target")); + for (int pos = 0; pos < adapterAccount.getCount(); pos++) if (adapterAccount.getItem(pos).id.equals(rule.account)) { spAccount.setSelection(pos); @@ -265,6 +288,9 @@ public class FragmentRule extends FragmentBase { adapterFolder.clear(); adapterFolder.addAll(folders); + adapterTarget.clear(); + adapterTarget.addAll(folders); + long account = args.getLong("account"); if (account == (Long) spAccount.getTag()) { Long folder = (Long) spFolder.getTag(); @@ -273,8 +299,17 @@ public class FragmentRule extends FragmentBase { spFolder.setSelection(pos); break; } - } else + + Long target = (Long) spTarget.getTag(); + for (int pos = 0; pos < folders.size(); pos++) + if (folders.get(pos).id.equals(target)) { + spTarget.setSelection(pos); + break; + } + } else { spFolder.setSelection(0); + spTarget.setSelection(0); + } grpReady.setVisibility(View.VISIBLE); bottom_navigation.setVisibility(View.VISIBLE); @@ -352,8 +387,13 @@ public class FragmentRule extends FragmentBase { Action action = (Action) spAction.getSelectedItem(); JSONObject jaction = new JSONObject(); - if (action != null) + if (action != null) { jaction.put("type", action.type); + if (action.type == EntityRule.TYPE_MOVE) { + EntityFolder target = (EntityFolder) spTarget.getSelectedItem(); + jaction.put("target", target.id); + } + } Bundle args = new Bundle(); args.putLong("id", id); diff --git a/app/src/main/java/eu/faircode/email/FragmentRules.java b/app/src/main/java/eu/faircode/email/FragmentRules.java index ca2c8a954d..b39a68762f 100644 --- a/app/src/main/java/eu/faircode/email/FragmentRules.java +++ b/app/src/main/java/eu/faircode/email/FragmentRules.java @@ -88,9 +88,9 @@ public class FragmentRules extends FragmentBase { super.onActivityCreated(savedInstanceState); DB db = DB.getInstance(getContext()); - db.rule().liveRules().observe(getViewLifecycleOwner(), new Observer>() { + db.rule().liveRules().observe(getViewLifecycleOwner(), new Observer>() { @Override - public void onChanged(List rules) { + public void onChanged(List rules) { if (rules == null) rules = new ArrayList<>(); diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 53391ba12d..e4186c9ca8 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -1037,7 +1037,9 @@ public class ServiceSynchronize extends LifecycleService { db.beginTransaction(); downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, - message.id, db.folder().getFolderDownload(folder.id)); + message.id, + db.folder().getFolderDownload(folder.id), + db.rule().getEnabledRules(folder.id)); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -1124,7 +1126,9 @@ public class ServiceSynchronize extends LifecycleService { db.beginTransaction(); downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), - message.id, db.folder().getFolderDownload(folder.id)); + message.id, + db.folder().getFolderDownload(folder.id), + db.rule().getEnabledRules(folder.id)); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -2300,6 +2304,8 @@ public class ServiceSynchronize extends LifecycleService { db.folder().setFolderSyncState(folder.id, "downloading"); + List rules = db.rule().getEnabledRules(folder.id); + //fp.add(IMAPFolder.FetchProfileItem.MESSAGE); // Download messages/attachments @@ -2317,7 +2323,7 @@ public class ServiceSynchronize extends LifecycleService { downloadMessage( this, folder, ifolder, (IMAPMessage) isub[j], - ids[from + j], download); + ids[from + j], download, rules); db.setTransactionSuccessful(); } catch (FolderClosedException ex) { throw ex; @@ -2575,7 +2581,7 @@ public class ServiceSynchronize extends LifecycleService { static void downloadMessage( Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, - long id, boolean download) throws MessagingException, IOException { + long id, boolean download, List rules) throws MessagingException, IOException { DB db = DB.getInstance(context); EntityMessage message = db.message().getMessage(id); if (message == null) @@ -2584,7 +2590,7 @@ public class ServiceSynchronize extends LifecycleService { if (message.setContactInfo(context)) db.message().updateMessage(message); - if (download) { + if (download || rules.size() > 0) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); long maxSize = prefs.getInt("download", 32768); if (maxSize == 0) @@ -2624,7 +2630,7 @@ public class ServiceSynchronize extends LifecycleService { MessageHelper.MessageParts parts = helper.getMessageParts(); - if (!message.content) + if (!message.content) { if (!metered || (message.size != null && message.size < maxSize)) { String body = parts.getHtml(context); message.write(context, body); @@ -2633,6 +2639,11 @@ public class ServiceSynchronize extends LifecycleService { Log.i(folder.name + " downloaded message id=" + message.id + " size=" + message.size); } + for (EntityRule rule : rules) + if (rule.matches(context, message)) + rule.execute(context, db, message); + } + for (int i = 0; i < attachments.size(); i++) { EntityAttachment attachment = attachments.get(i); if (!attachment.available) diff --git a/app/src/main/java/eu/faircode/email/TupleRuleEx.java b/app/src/main/java/eu/faircode/email/TupleRuleEx.java index fa5e71e439..45dac45a9d 100644 --- a/app/src/main/java/eu/faircode/email/TupleRuleEx.java +++ b/app/src/main/java/eu/faircode/email/TupleRuleEx.java @@ -21,13 +21,17 @@ package eu.faircode.email; public class TupleRuleEx extends EntityRule { public long account; + public String folderName; + public String accountName; @Override public boolean equals(Object obj) { if (obj instanceof TupleRuleEx) { TupleRuleEx other = (TupleRuleEx) obj; return (super.equals(obj) && - this.account == other.account); + this.account == other.account && + (this.folderName == null ? other.folderName == null : this.folderName.equals(other.folderName)) && + (this.accountName == null ? other.accountName == null : this.accountName.equals(other.accountName))); } else return false; } diff --git a/app/src/main/java/eu/faircode/email/ViewModelBrowse.java b/app/src/main/java/eu/faircode/email/ViewModelBrowse.java index 3121de01bb..d103c5cdd7 100644 --- a/app/src/main/java/eu/faircode/email/ViewModelBrowse.java +++ b/app/src/main/java/eu/faircode/email/ViewModelBrowse.java @@ -26,6 +26,7 @@ import com.sun.mail.imap.IMAPMessage; import com.sun.mail.imap.IMAPStore; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; @@ -230,7 +231,8 @@ public class ViewModelBrowse extends ViewModel { message = ServiceSynchronize.synchronizeMessage(state.context, folder, state.ifolder, (IMAPMessage) isub[j], true); ServiceSynchronize.downloadMessage(state.context, - folder, state.ifolder, (IMAPMessage) isub[j], message.id, false); + folder, state.ifolder, (IMAPMessage) isub[j], message.id, + false, new ArrayList()); count++; } db.message().setMessageFound(message.account, message.thread); diff --git a/app/src/main/res/layout/fragment_rule.xml b/app/src/main/res/layout/fragment_rule.xml index f4e90c4ec5..f9485acf8b 100644 --- a/app/src/main/res/layout/fragment_rule.xml +++ b/app/src/main/res/layout/fragment_rule.xml @@ -198,7 +198,7 @@ app:layout_constraintTop_toBottomOf="@id/tvAction" /> - - + app:layout_constraintTop_toBottomOf="@id/tvTarget" /> + app:constraint_referenced_ids="tvName,etName,tvOrder,etOrder,cbEnabled,tvAccount,spAccount,tvFolder,spFolder,tvFolderRemark,tvSender,etSender,tvSubject,etSubject,tvText,etText,tvAction,spAction,tvTarget,spTarget" /> + app:constraint_referenced_ids="tvTarget,spTarget" /> diff --git a/app/src/main/res/layout/item_rule.xml b/app/src/main/res/layout/item_rule.xml index 0f086eb8b9..37465191ed 100644 --- a/app/src/main/res/layout/item_rule.xml +++ b/app/src/main/res/layout/item_rule.xml @@ -25,6 +25,22 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@id/tvFolder" /> \ No newline at end of file