mirror of
https://github.com/M66B/FairEmail.git
synced 2025-01-02 21:24:34 +00:00
PoC rule expressions
This commit is contained in:
parent
27e955a13f
commit
9b656434bd
8 changed files with 189 additions and 4 deletions
|
@ -57,3 +57,4 @@ FairEmail uses parts or all of:
|
||||||
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
||||||
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
|
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
|
||||||
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
||||||
|
* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).
|
||||||
|
|
|
@ -587,6 +587,7 @@ dependencies {
|
||||||
def ws_version = "2.14"
|
def ws_version = "2.14"
|
||||||
def tinylog_version = "2.6.2"
|
def tinylog_version = "2.6.2"
|
||||||
def zxing_version = "3.5.3"
|
def zxing_version = "3.5.3"
|
||||||
|
def evalex_version = "3.2.0"
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google
|
// https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
|
||||||
|
@ -857,4 +858,7 @@ dependencies {
|
||||||
// https://github.com/zxing/zxing
|
// https://github.com/zxing/zxing
|
||||||
// https://mvnrepository.com/artifact/com.google.zxing/core
|
// https://mvnrepository.com/artifact/com.google.zxing/core
|
||||||
implementation "com.google.zxing:core:$zxing_version"
|
implementation "com.google.zxing:core:$zxing_version"
|
||||||
|
|
||||||
|
// https://github.com/ezylang/EvalEx
|
||||||
|
implementation "com.ezylang:EvalEx:$evalex_version"
|
||||||
}
|
}
|
||||||
|
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
@ -161,3 +161,6 @@
|
||||||
-dontwarn java.lang.**
|
-dontwarn java.lang.**
|
||||||
-dontwarn javax.naming.**
|
-dontwarn javax.naming.**
|
||||||
-dontwarn sun.reflect.Reflection
|
-dontwarn sun.reflect.Reflection
|
||||||
|
|
||||||
|
#EvalEx
|
||||||
|
-dontwarn lombok.Generated
|
|
@ -57,3 +57,4 @@ FairEmail uses parts or all of:
|
||||||
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
||||||
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
|
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
|
||||||
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
||||||
|
* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).
|
||||||
|
|
|
@ -21,6 +21,8 @@ package eu.faircode.email;
|
||||||
|
|
||||||
import static androidx.room.ForeignKey.CASCADE;
|
import static androidx.room.ForeignKey.CASCADE;
|
||||||
|
|
||||||
|
import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -41,6 +43,15 @@ import androidx.room.ForeignKey;
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
import com.ezylang.evalex.EvaluationException;
|
||||||
|
import com.ezylang.evalex.Expression;
|
||||||
|
import com.ezylang.evalex.config.ExpressionConfiguration;
|
||||||
|
import com.ezylang.evalex.data.EvaluationValue;
|
||||||
|
import com.ezylang.evalex.operators.AbstractOperator;
|
||||||
|
import com.ezylang.evalex.operators.InfixOperator;
|
||||||
|
import com.ezylang.evalex.parser.ParseException;
|
||||||
|
import com.ezylang.evalex.parser.Token;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
|
@ -62,6 +73,7 @@ import java.util.Date;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
@ -151,6 +163,10 @@ public class EntityRule {
|
||||||
private static final int MAX_NOTES_LENGTH = 512; // characters
|
private static final int MAX_NOTES_LENGTH = 512; // characters
|
||||||
private static final int URL_TIMEOUT = 15 * 1000; // milliseconds
|
private static final int URL_TIMEOUT = 15 * 1000; // milliseconds
|
||||||
|
|
||||||
|
private static final List<String> EXPR_VARIABLES = Collections.unmodifiableList(Arrays.asList(
|
||||||
|
"sender", "subject"
|
||||||
|
));
|
||||||
|
|
||||||
static boolean needsHeaders(EntityMessage message, List<EntityRule> rules) {
|
static boolean needsHeaders(EntityMessage message, List<EntityRule> rules) {
|
||||||
return needsHeaders(rules);
|
return needsHeaders(rules);
|
||||||
}
|
}
|
||||||
|
@ -183,6 +199,16 @@ public class EntityRule {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (jcondition.has("expression")) {
|
||||||
|
Expression expression = getExpression(rule, null);
|
||||||
|
if (expression != null)
|
||||||
|
for (String variable : expression.getUsedVariables())
|
||||||
|
if ("body".equals(what) && "body".equalsIgnoreCase(variable))
|
||||||
|
return true;
|
||||||
|
else if ("header".equals(what) && "header".equalsIgnoreCase(variable))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
Log.e(ex);
|
Log.e(ex);
|
||||||
}
|
}
|
||||||
|
@ -463,6 +489,19 @@ public class EntityRule {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expression expression = getExpression(this, message);
|
||||||
|
if (expression != null) {
|
||||||
|
for (String variable : expression.getUsedVariables())
|
||||||
|
if ("header".equalsIgnoreCase(variable) && message.headers == null)
|
||||||
|
throw new IllegalArgumentException(context.getString(R.string.title_rule_no_headers));
|
||||||
|
|
||||||
|
Log.i("EXPR evaluating " + jcondition.getString("expression"));
|
||||||
|
Boolean result = expression.evaluate().getBooleanValue();
|
||||||
|
Log.i("EXPR evaluated=" + result);
|
||||||
|
if (!Boolean.TRUE.equals(result))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Safeguard
|
// Safeguard
|
||||||
if (jsender == null &&
|
if (jsender == null &&
|
||||||
jrecipient == null &&
|
jrecipient == null &&
|
||||||
|
@ -472,9 +511,10 @@ public class EntityRule {
|
||||||
jbody == null &&
|
jbody == null &&
|
||||||
jdate == null &&
|
jdate == null &&
|
||||||
jschedule == null &&
|
jschedule == null &&
|
||||||
!jcondition.has("younger"))
|
!jcondition.has("younger") &&
|
||||||
|
!jcondition.has("expression"))
|
||||||
return false;
|
return false;
|
||||||
} catch (JSONException ex) {
|
} catch (JSONException | ParseException | EvaluationException ex) {
|
||||||
Log.e(ex);
|
Log.e(ex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -593,6 +633,58 @@ public class EntityRule {
|
||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
|
||||||
|
public static class ContainsOperator extends AbstractOperator {
|
||||||
|
@Override
|
||||||
|
public EvaluationValue evaluate(
|
||||||
|
Expression expression, Token operatorToken, EvaluationValue... operands) {
|
||||||
|
String op1 = operands[0].getStringValue();
|
||||||
|
String op2 = operands[1].getStringValue();
|
||||||
|
Log.i("EXPR " + op1 + " CONTAINS " + op2);
|
||||||
|
return expression.convertValue(
|
||||||
|
!TextUtils.isEmpty(op1) && !TextUtils.isEmpty(op2) &&
|
||||||
|
op1.toLowerCase().contains(op2.toLowerCase()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
|
||||||
|
public static class MatchesOperator extends AbstractOperator {
|
||||||
|
@Override
|
||||||
|
public EvaluationValue evaluate(
|
||||||
|
Expression expression, Token operatorToken, EvaluationValue... operands) {
|
||||||
|
String op1 = operands[0].getStringValue();
|
||||||
|
String op2 = operands[1].getStringValue();
|
||||||
|
Log.i("EXPR " + op1 + " MATCHES " + op2);
|
||||||
|
return expression.convertValue(
|
||||||
|
!TextUtils.isEmpty(op1) && !TextUtils.isEmpty(op2) &&
|
||||||
|
Pattern.compile(op2, Pattern.DOTALL).matcher(op1).matches());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Expression getExpression(EntityRule rule, EntityMessage message) throws JSONException {
|
||||||
|
// https://ezylang.github.io/EvalEx/
|
||||||
|
|
||||||
|
JSONObject jcondition = new JSONObject(rule.condition);
|
||||||
|
if (!jcondition.has("expression"))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
String sender = null;
|
||||||
|
String subject = null;
|
||||||
|
if (message != null) {
|
||||||
|
if (message.from != null && message.from.length == 1)
|
||||||
|
sender = MessageHelper.formatAddresses(message.from);
|
||||||
|
subject = message.subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration();
|
||||||
|
configuration.getOperatorDictionary().addOperator("CONTAINS", new ContainsOperator());
|
||||||
|
configuration.getOperatorDictionary().addOperator("MATCHES", new MatchesOperator());
|
||||||
|
|
||||||
|
return new Expression(jcondition.getString("expression"), configuration)
|
||||||
|
.with("sender", sender)
|
||||||
|
.with("subject", subject);
|
||||||
|
}
|
||||||
|
|
||||||
boolean execute(Context context, EntityMessage message, String html) throws JSONException, IOException {
|
boolean execute(Context context, EntityMessage message, String html) throws JSONException, IOException {
|
||||||
boolean executed = _execute(context, message, html);
|
boolean executed = _execute(context, message, html);
|
||||||
if (this.id != null && executed) {
|
if (this.id != null && executed) {
|
||||||
|
@ -655,6 +747,25 @@ public class EntityRule {
|
||||||
}
|
}
|
||||||
|
|
||||||
void validate(Context context) throws JSONException, IllegalArgumentException {
|
void validate(Context context) throws JSONException, IllegalArgumentException {
|
||||||
|
Expression expression = getExpression(this, null);
|
||||||
|
if (expression != null)
|
||||||
|
try {
|
||||||
|
for (String variable : expression.getUsedVariables()) {
|
||||||
|
Log.i("EXPR variable=" + variable);
|
||||||
|
if (!EXPR_VARIABLES.contains(variable))
|
||||||
|
throw new IllegalArgumentException("Unknown variable '" + variable + "'");
|
||||||
|
}
|
||||||
|
Log.i("EXPR validating");
|
||||||
|
expression.validate();
|
||||||
|
Log.i("EXPR validated");
|
||||||
|
} catch (ParseException ex) {
|
||||||
|
Log.w("EXPR", ex);
|
||||||
|
String message = ex.getMessage();
|
||||||
|
if (TextUtils.isEmpty(message))
|
||||||
|
message = "Invalid expression";
|
||||||
|
throw new IllegalArgumentException(message, ex);
|
||||||
|
}
|
||||||
|
|
||||||
JSONObject jargs = new JSONObject(action);
|
JSONObject jargs = new JSONObject(action);
|
||||||
int type = jargs.getInt("type");
|
int type = jargs.getInt("type");
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,8 @@ public class FragmentRule extends FragmentBase {
|
||||||
private CheckBox cbEveryDay;
|
private CheckBox cbEveryDay;
|
||||||
private EditText etYounger;
|
private EditText etYounger;
|
||||||
|
|
||||||
|
private EditText etExpression;
|
||||||
|
|
||||||
private Spinner spAction;
|
private Spinner spAction;
|
||||||
private TextView tvActionRemark;
|
private TextView tvActionRemark;
|
||||||
|
|
||||||
|
@ -184,6 +186,7 @@ public class FragmentRule extends FragmentBase {
|
||||||
private ContentLoadingProgressBar pbWait;
|
private ContentLoadingProgressBar pbWait;
|
||||||
|
|
||||||
private Group grpReady;
|
private Group grpReady;
|
||||||
|
private Group grpExpression;
|
||||||
private Group grpAge;
|
private Group grpAge;
|
||||||
private Group grpSnooze;
|
private Group grpSnooze;
|
||||||
private Group grpFlag;
|
private Group grpFlag;
|
||||||
|
@ -333,6 +336,8 @@ public class FragmentRule extends FragmentBase {
|
||||||
cbEveryDay = view.findViewById(R.id.cbEveryDay);
|
cbEveryDay = view.findViewById(R.id.cbEveryDay);
|
||||||
etYounger = view.findViewById(R.id.etYounger);
|
etYounger = view.findViewById(R.id.etYounger);
|
||||||
|
|
||||||
|
etExpression = view.findViewById(R.id.etExpression);
|
||||||
|
|
||||||
spAction = view.findViewById(R.id.spAction);
|
spAction = view.findViewById(R.id.spAction);
|
||||||
tvActionRemark = view.findViewById(R.id.tvActionRemark);
|
tvActionRemark = view.findViewById(R.id.tvActionRemark);
|
||||||
|
|
||||||
|
@ -385,6 +390,7 @@ public class FragmentRule extends FragmentBase {
|
||||||
pbWait = view.findViewById(R.id.pbWait);
|
pbWait = view.findViewById(R.id.pbWait);
|
||||||
|
|
||||||
grpReady = view.findViewById(R.id.grpReady);
|
grpReady = view.findViewById(R.id.grpReady);
|
||||||
|
grpExpression = view.findViewById(R.id.grpExpression);
|
||||||
grpAge = view.findViewById(R.id.grpAge);
|
grpAge = view.findViewById(R.id.grpAge);
|
||||||
grpSnooze = view.findViewById(R.id.grpSnooze);
|
grpSnooze = view.findViewById(R.id.grpSnooze);
|
||||||
grpFlag = view.findViewById(R.id.grpFlag);
|
grpFlag = view.findViewById(R.id.grpFlag);
|
||||||
|
@ -854,6 +860,7 @@ public class FragmentRule extends FragmentBase {
|
||||||
tvFolder.setText(null);
|
tvFolder.setText(null);
|
||||||
bottom_navigation.setVisibility(View.GONE);
|
bottom_navigation.setVisibility(View.GONE);
|
||||||
grpReady.setVisibility(View.GONE);
|
grpReady.setVisibility(View.GONE);
|
||||||
|
grpExpression.setVisibility(View.GONE);
|
||||||
grpAge.setVisibility(View.GONE);
|
grpAge.setVisibility(View.GONE);
|
||||||
grpSnooze.setVisibility(View.GONE);
|
grpSnooze.setVisibility(View.GONE);
|
||||||
grpFlag.setVisibility(View.GONE);
|
grpFlag.setVisibility(View.GONE);
|
||||||
|
@ -1286,6 +1293,8 @@ public class FragmentRule extends FragmentBase {
|
||||||
etYounger.setText(jcondition.has("younger")
|
etYounger.setText(jcondition.has("younger")
|
||||||
? Integer.toString(jcondition.optInt("younger")) : null);
|
? Integer.toString(jcondition.optInt("younger")) : null);
|
||||||
|
|
||||||
|
etExpression.setText(jcondition.optString("expression"));
|
||||||
|
|
||||||
spScheduleDayStart.setSelection(start / (24 * 60));
|
spScheduleDayStart.setSelection(start / (24 * 60));
|
||||||
spScheduleDayEnd.setSelection(end / (24 * 60));
|
spScheduleDayEnd.setSelection(end / (24 * 60));
|
||||||
|
|
||||||
|
@ -1423,6 +1432,7 @@ public class FragmentRule extends FragmentBase {
|
||||||
Log.e(ex);
|
Log.e(ex);
|
||||||
} finally {
|
} finally {
|
||||||
grpReady.setVisibility(View.VISIBLE);
|
grpReady.setVisibility(View.VISIBLE);
|
||||||
|
grpExpression.setVisibility(BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
||||||
grpAge.setVisibility(cbDaily.isChecked() ? View.VISIBLE : View.GONE);
|
grpAge.setVisibility(cbDaily.isChecked() ? View.VISIBLE : View.GONE);
|
||||||
if (id < 0)
|
if (id < 0)
|
||||||
bottom_navigation.getMenu().removeItem(R.id.action_delete);
|
bottom_navigation.getMenu().removeItem(R.id.action_delete);
|
||||||
|
@ -1561,7 +1571,9 @@ public class FragmentRule extends FragmentBase {
|
||||||
jheader == null &&
|
jheader == null &&
|
||||||
jbody == null &&
|
jbody == null &&
|
||||||
jdate == null &&
|
jdate == null &&
|
||||||
jschedule == null)
|
jschedule == null &&
|
||||||
|
!jcondition.has("younger") &&
|
||||||
|
!jcondition.has("expression"))
|
||||||
throw new IllegalArgumentException(context.getString(R.string.title_rule_condition_missing));
|
throw new IllegalArgumentException(context.getString(R.string.title_rule_condition_missing));
|
||||||
|
|
||||||
if (TextUtils.isEmpty(order))
|
if (TextUtils.isEmpty(order))
|
||||||
|
@ -1729,6 +1741,10 @@ public class FragmentRule extends FragmentBase {
|
||||||
Log.e(ex);
|
Log.e(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String expression = etExpression.getText().toString().trim();
|
||||||
|
if (!TextUtils.isEmpty(expression))
|
||||||
|
jcondition.put("expression", expression);
|
||||||
|
|
||||||
return jcondition;
|
return jcondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -818,6 +818,54 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tvYounger" />
|
app:layout_constraintTop_toBottomOf="@id/tvYounger" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAndExpression"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/title_rule_and"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/etYounger" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/vSeparatorExpression"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/colorSeparator"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvAndExpression" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvExpression"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/title_rule_expression"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/vSeparatorExpression" />
|
||||||
|
|
||||||
|
<eu.faircode.email.EditTextPlain
|
||||||
|
android:id="@+id/etExpression"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/title_optional"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvExpression" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/grpExpression"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:constraint_referenced_ids="tvAndExpression,vSeparatorExpression,tvExpression,etExpression" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvAction"
|
android:id="@+id/tvAction"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -828,7 +876,7 @@
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/etYounger" />
|
app:layout_constraintTop_toBottomOf="@+id/etExpression" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/vSeparatorAction"
|
android:id="@+id/vSeparatorAction"
|
||||||
|
|
|
@ -2044,6 +2044,7 @@
|
||||||
<string name="title_rule_time_rel">Relative time (received) between</string>
|
<string name="title_rule_time_rel">Relative time (received) between</string>
|
||||||
<string name="title_rule_time_every_day">Every day</string>
|
<string name="title_rule_time_every_day">Every day</string>
|
||||||
<string name="title_rule_younger">Messages younger than (hours)</string>
|
<string name="title_rule_younger">Messages younger than (hours)</string>
|
||||||
|
<string name="title_rule_expression" translatable="false">Expression</string>
|
||||||
<string name="title_rule_regex">Regex</string>
|
<string name="title_rule_regex">Regex</string>
|
||||||
<string name="title_rule_and">AND</string>
|
<string name="title_rule_and">AND</string>
|
||||||
<string name="title_rule_not">NOT</string>
|
<string name="title_rule_not">NOT</string>
|
||||||
|
|
Loading…
Reference in a new issue