diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5a08d7a..b6e35e3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -81,6 +81,11 @@
+
{
+ Intent filterIntent = new Intent(MainActivity.this, SMSFilterActivity.class);
+ startActivity(filterIntent);
+ });
}
private void renderAvailableSimOptions() {
diff --git a/android/app/src/main/java/com/vernu/sms/activities/SMSFilterActivity.java b/android/app/src/main/java/com/vernu/sms/activities/SMSFilterActivity.java
new file mode 100644
index 0000000..832e0c2
--- /dev/null
+++ b/android/app/src/main/java/com/vernu/sms/activities/SMSFilterActivity.java
@@ -0,0 +1,267 @@
+package com.vernu.sms.activities;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.textfield.TextInputEditText;
+import com.vernu.sms.R;
+import com.vernu.sms.helpers.SMSFilterHelper;
+import com.vernu.sms.models.SMSFilterRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SMSFilterActivity extends AppCompatActivity {
+ private Context mContext;
+ private Switch filterEnabledSwitch;
+ private RadioGroup filterModeRadioGroup;
+ private RadioButton allowListRadio;
+ private RadioButton blockListRadio;
+ private RecyclerView filterRulesRecyclerView;
+ private FloatingActionButton addRuleFab;
+ private FilterRulesAdapter adapter;
+ private SMSFilterHelper.FilterConfig filterConfig;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_sms_filter);
+
+ mContext = getApplicationContext();
+
+ // Initialize views
+ ImageButton backButton = findViewById(R.id.backButton);
+ filterEnabledSwitch = findViewById(R.id.filterEnabledSwitch);
+ filterModeRadioGroup = findViewById(R.id.filterModeRadioGroup);
+ allowListRadio = findViewById(R.id.allowListRadio);
+ blockListRadio = findViewById(R.id.blockListRadio);
+ filterRulesRecyclerView = findViewById(R.id.filterRulesRecyclerView);
+ addRuleFab = findViewById(R.id.addRuleFab);
+
+ // Setup back button
+ backButton.setOnClickListener(v -> finish());
+
+ // Load filter config
+ filterConfig = SMSFilterHelper.loadFilterConfig(mContext);
+
+ // Setup RecyclerView
+ adapter = new FilterRulesAdapter(filterConfig.getRules());
+ filterRulesRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ filterRulesRecyclerView.setAdapter(adapter);
+
+ // Load current settings
+ filterEnabledSwitch.setChecked(filterConfig.isEnabled());
+ // Default to block list if mode is not set
+ if (filterConfig.getMode() == SMSFilterHelper.FilterMode.ALLOW_LIST) {
+ allowListRadio.setChecked(true);
+ } else {
+ blockListRadio.setChecked(true);
+ }
+
+ // Setup listeners
+ filterEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ filterConfig.setEnabled(isChecked);
+ saveFilterConfig();
+ });
+
+ filterModeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (checkedId == R.id.allowListRadio) {
+ filterConfig.setMode(SMSFilterHelper.FilterMode.ALLOW_LIST);
+ } else {
+ filterConfig.setMode(SMSFilterHelper.FilterMode.BLOCK_LIST);
+ }
+ saveFilterConfig();
+ });
+
+ addRuleFab.setOnClickListener(v -> showAddEditRuleDialog(-1));
+ }
+
+ private void saveFilterConfig() {
+ SMSFilterHelper.saveFilterConfig(mContext, filterConfig);
+ }
+
+ private void showAddEditRuleDialog(int position) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_filter_rule, null);
+ builder.setView(dialogView);
+
+ TextInputEditText patternEditText = dialogView.findViewById(R.id.patternEditText);
+ Spinner filterTargetSpinner = dialogView.findViewById(R.id.filterTargetSpinner);
+ Spinner matchTypeSpinner = dialogView.findViewById(R.id.matchTypeSpinner);
+ Switch caseSensitiveSwitch = dialogView.findViewById(R.id.caseSensitiveSwitch);
+ Button cancelButton = dialogView.findViewById(R.id.cancelButton);
+ Button saveButton = dialogView.findViewById(R.id.saveButton);
+
+ // Setup filter target spinner
+ String[] filterTargets = {"Sender", "Message", "Both"};
+ ArrayAdapter targetAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, filterTargets);
+ targetAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ filterTargetSpinner.setAdapter(targetAdapter);
+
+ // Setup match type spinner
+ String[] matchTypes = {"Exact Match", "Starts With", "Ends With", "Contains"};
+ ArrayAdapter spinnerAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, matchTypes);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ matchTypeSpinner.setAdapter(spinnerAdapter);
+
+ // If editing, populate fields
+ boolean isEdit = position >= 0;
+ TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle);
+ if (isEdit) {
+ SMSFilterRule rule = filterConfig.getRules().get(position);
+ patternEditText.setText(rule.getPattern());
+ filterTargetSpinner.setSelection(rule.getFilterTarget().ordinal());
+ matchTypeSpinner.setSelection(rule.getMatchType().ordinal());
+ caseSensitiveSwitch.setChecked(rule.isCaseSensitive());
+ if (dialogTitle != null) {
+ dialogTitle.setText("Edit Filter Rule");
+ }
+ } else {
+ // Default to case insensitive
+ caseSensitiveSwitch.setChecked(false);
+ }
+
+ AlertDialog dialog = builder.create();
+
+ cancelButton.setOnClickListener(v -> dialog.dismiss());
+
+ saveButton.setOnClickListener(v -> {
+ String pattern = patternEditText.getText() != null ? patternEditText.getText().toString().trim() : "";
+ if (pattern.isEmpty()) {
+ Toast.makeText(this, "Please enter a pattern", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ SMSFilterRule.FilterTarget filterTarget = SMSFilterRule.FilterTarget.values()[filterTargetSpinner.getSelectedItemPosition()];
+ SMSFilterRule.MatchType matchType = SMSFilterRule.MatchType.values()[matchTypeSpinner.getSelectedItemPosition()];
+ boolean caseSensitive = caseSensitiveSwitch.isChecked();
+
+ if (isEdit) {
+ SMSFilterRule rule = filterConfig.getRules().get(position);
+ rule.setPattern(pattern);
+ rule.setFilterTarget(filterTarget);
+ rule.setMatchType(matchType);
+ rule.setCaseSensitive(caseSensitive);
+ adapter.notifyItemChanged(position);
+ } else {
+ SMSFilterRule newRule = new SMSFilterRule(pattern, matchType, filterTarget, caseSensitive);
+ filterConfig.getRules().add(newRule);
+ adapter.notifyItemInserted(filterConfig.getRules().size() - 1);
+ }
+
+ saveFilterConfig();
+ dialog.dismiss();
+ });
+
+ dialog.show();
+ }
+
+ private void deleteRule(int position) {
+ new AlertDialog.Builder(this)
+ .setTitle("Delete Rule")
+ .setMessage("Are you sure you want to delete this filter rule?")
+ .setPositiveButton("Delete", (dialog, which) -> {
+ filterConfig.getRules().remove(position);
+ adapter.notifyItemRemoved(position);
+ saveFilterConfig();
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+
+ private class FilterRulesAdapter extends RecyclerView.Adapter {
+ private List rules;
+
+ public FilterRulesAdapter(List rules) {
+ this.rules = rules != null ? rules : new ArrayList<>();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_filter_rule, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ SMSFilterRule rule = rules.get(position);
+ holder.patternText.setText(rule.getPattern());
+
+ String matchTypeText = "";
+ switch (rule.getMatchType()) {
+ case EXACT:
+ matchTypeText = "Exact Match";
+ break;
+ case STARTS_WITH:
+ matchTypeText = "Starts With";
+ break;
+ case ENDS_WITH:
+ matchTypeText = "Ends With";
+ break;
+ case CONTAINS:
+ matchTypeText = "Contains";
+ break;
+ }
+ holder.matchTypeText.setText(matchTypeText);
+
+ String filterTargetText = "";
+ switch (rule.getFilterTarget()) {
+ case SENDER:
+ filterTargetText = "Filter: Sender";
+ break;
+ case MESSAGE:
+ filterTargetText = "Filter: Message";
+ break;
+ case BOTH:
+ filterTargetText = "Filter: Sender or Message";
+ break;
+ }
+ String caseText = rule.isCaseSensitive() ? " (Case Sensitive)" : " (Case Insensitive)";
+ holder.filterTargetText.setText(filterTargetText + caseText);
+
+ holder.editButton.setOnClickListener(v -> showAddEditRuleDialog(position));
+ holder.deleteButton.setOnClickListener(v -> deleteRule(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return rules.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ TextView patternText;
+ TextView matchTypeText;
+ TextView filterTargetText;
+ ImageButton editButton;
+ ImageButton deleteButton;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ patternText = itemView.findViewById(R.id.patternText);
+ matchTypeText = itemView.findViewById(R.id.matchTypeText);
+ filterTargetText = itemView.findViewById(R.id.filterTargetText);
+ editButton = itemView.findViewById(R.id.editButton);
+ deleteButton = itemView.findViewById(R.id.deleteButton);
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/vernu/sms/helpers/SMSFilterHelper.java b/android/app/src/main/java/com/vernu/sms/helpers/SMSFilterHelper.java
new file mode 100644
index 0000000..999713e
--- /dev/null
+++ b/android/app/src/main/java/com/vernu/sms/helpers/SMSFilterHelper.java
@@ -0,0 +1,142 @@
+package com.vernu.sms.helpers;
+
+import android.content.Context;
+import android.util.Log;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.vernu.sms.AppConstants;
+import com.vernu.sms.models.SMSFilterRule;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SMSFilterHelper {
+ private static final String TAG = "SMSFilterHelper";
+
+ public enum FilterMode {
+ ALLOW_LIST,
+ BLOCK_LIST
+ }
+
+ public static class FilterConfig {
+ private boolean enabled = false;
+ private FilterMode mode = FilterMode.BLOCK_LIST; // Default to block list
+ private List rules = new ArrayList<>();
+
+ public FilterConfig() {
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public FilterMode getMode() {
+ return mode;
+ }
+
+ public void setMode(FilterMode mode) {
+ this.mode = mode;
+ }
+
+ public List getRules() {
+ return rules;
+ }
+
+ public void setRules(List rules) {
+ this.rules = rules != null ? rules : new ArrayList<>();
+ }
+ }
+
+ /**
+ * Load filter configuration from SharedPreferences
+ */
+ public static FilterConfig loadFilterConfig(Context context) {
+ String json = SharedPreferenceHelper.getSharedPreferenceString(
+ context,
+ AppConstants.SHARED_PREFS_SMS_FILTER_CONFIG_KEY,
+ null
+ );
+
+ if (json == null || json.isEmpty()) {
+ return new FilterConfig();
+ }
+
+ try {
+ Gson gson = new Gson();
+ Type type = new TypeToken() {}.getType();
+ FilterConfig config = gson.fromJson(json, type);
+ return config != null ? config : new FilterConfig();
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading filter config: " + e.getMessage());
+ return new FilterConfig();
+ }
+ }
+
+ /**
+ * Save filter configuration to SharedPreferences
+ */
+ public static void saveFilterConfig(Context context, FilterConfig config) {
+ try {
+ Gson gson = new Gson();
+ String json = gson.toJson(config);
+ SharedPreferenceHelper.setSharedPreferenceString(
+ context,
+ AppConstants.SHARED_PREFS_SMS_FILTER_CONFIG_KEY,
+ json
+ );
+ } catch (Exception e) {
+ Log.e(TAG, "Error saving filter config: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Check if an SMS should be processed based on filter configuration
+ * @param sender The sender phone number
+ * @param message The message content
+ * @param context Application context
+ * @return true if SMS should be processed, false if it should be filtered out
+ */
+ public static boolean shouldProcessSMS(String sender, String message, Context context) {
+ FilterConfig config = loadFilterConfig(context);
+
+ // If filter is disabled, process all SMS
+ if (!config.isEnabled()) {
+ return true;
+ }
+
+ // If no rules, process all SMS (empty filter doesn't block anything)
+ if (config.getRules() == null || config.getRules().isEmpty()) {
+ return true;
+ }
+
+ // Check if sender and/or message matches any rule
+ boolean matchesAnyRule = false;
+ for (SMSFilterRule rule : config.getRules()) {
+ if (rule.matches(sender, message)) {
+ matchesAnyRule = true;
+ break;
+ }
+ }
+
+ // Apply filter mode
+ if (config.getMode() == FilterMode.ALLOW_LIST) {
+ // Only process if matches a rule
+ return matchesAnyRule;
+ } else {
+ // Block list: process if it does NOT match any rule
+ return !matchesAnyRule;
+ }
+ }
+
+ /**
+ * Legacy method for backward compatibility - checks sender only
+ */
+ public static boolean shouldProcessSMS(String sender, Context context) {
+ return shouldProcessSMS(sender, null, context);
+ }
+}
diff --git a/android/app/src/main/java/com/vernu/sms/models/SMSFilterRule.java b/android/app/src/main/java/com/vernu/sms/models/SMSFilterRule.java
new file mode 100644
index 0000000..97574e2
--- /dev/null
+++ b/android/app/src/main/java/com/vernu/sms/models/SMSFilterRule.java
@@ -0,0 +1,135 @@
+package com.vernu.sms.models;
+
+public class SMSFilterRule {
+ public enum MatchType {
+ EXACT,
+ STARTS_WITH,
+ ENDS_WITH,
+ CONTAINS
+ }
+
+ public enum FilterTarget {
+ SENDER,
+ MESSAGE,
+ BOTH
+ }
+
+ private String pattern;
+ private MatchType matchType;
+ private FilterTarget filterTarget = FilterTarget.SENDER; // Default to sender for backward compatibility
+ private boolean caseSensitive = false; // Default to case insensitive
+
+ public SMSFilterRule() {
+ }
+
+ public SMSFilterRule(String pattern, MatchType matchType) {
+ this.pattern = pattern;
+ this.matchType = matchType;
+ this.filterTarget = FilterTarget.SENDER;
+ this.caseSensitive = false;
+ }
+
+ public SMSFilterRule(String pattern, MatchType matchType, FilterTarget filterTarget) {
+ this.pattern = pattern;
+ this.matchType = matchType;
+ this.filterTarget = filterTarget;
+ this.caseSensitive = false;
+ }
+
+ public SMSFilterRule(String pattern, MatchType matchType, FilterTarget filterTarget, boolean caseSensitive) {
+ this.pattern = pattern;
+ this.matchType = matchType;
+ this.filterTarget = filterTarget;
+ this.caseSensitive = caseSensitive;
+ }
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public void setPattern(String pattern) {
+ this.pattern = pattern;
+ }
+
+ public MatchType getMatchType() {
+ return matchType;
+ }
+
+ public void setMatchType(MatchType matchType) {
+ this.matchType = matchType;
+ }
+
+ public FilterTarget getFilterTarget() {
+ return filterTarget;
+ }
+
+ public void setFilterTarget(FilterTarget filterTarget) {
+ this.filterTarget = filterTarget != null ? filterTarget : FilterTarget.SENDER;
+ }
+
+ public boolean isCaseSensitive() {
+ return caseSensitive;
+ }
+
+ public void setCaseSensitive(boolean caseSensitive) {
+ this.caseSensitive = caseSensitive;
+ }
+
+ /**
+ * Check if a string matches this filter rule based on match type
+ */
+ private boolean matchesString(String text) {
+ if (pattern == null || text == null) {
+ return false;
+ }
+
+ String patternToMatch = pattern;
+ String textToMatch = text;
+
+ // Apply case sensitivity
+ if (!caseSensitive) {
+ patternToMatch = patternToMatch.toLowerCase();
+ textToMatch = textToMatch.toLowerCase();
+ }
+
+ switch (matchType) {
+ case EXACT:
+ return textToMatch.equals(patternToMatch);
+ case STARTS_WITH:
+ return textToMatch.startsWith(patternToMatch);
+ case ENDS_WITH:
+ return textToMatch.endsWith(patternToMatch);
+ case CONTAINS:
+ return textToMatch.contains(patternToMatch);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check if the given sender and/or message matches this filter rule
+ */
+ public boolean matches(String sender, String message) {
+ if (pattern == null) {
+ return false;
+ }
+
+ switch (filterTarget) {
+ case SENDER:
+ return matchesString(sender);
+ case MESSAGE:
+ return matchesString(message);
+ case BOTH:
+ return matchesString(sender) || matchesString(message);
+ default:
+ return matchesString(sender);
+ }
+ }
+
+ /**
+ * Legacy method for backward compatibility - checks sender only
+ */
+ public boolean matches(String sender) {
+ return matches(sender, null);
+ }
+}
diff --git a/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java b/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java
index f3c0cb9..fb3c50a 100644
--- a/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java
+++ b/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java
@@ -9,6 +9,7 @@ import android.util.Log;
import com.vernu.sms.AppConstants;
import com.vernu.sms.dtos.SMSDTO;
import com.vernu.sms.helpers.SharedPreferenceHelper;
+import com.vernu.sms.helpers.SMSFilterHelper;
import com.vernu.sms.workers.SMSReceivedWorker;
import java.security.MessageDigest;
@@ -67,6 +68,14 @@ public class SMSBroadcastReceiver extends BroadcastReceiver {
// receivedSMSDTO.setMessage(receivedSMS.getMessage());
// receivedSMSDTO.setReceivedAt(receivedSMS.getReceivedAt());
+ // Apply SMS filter
+ String sender = receivedSMSDTO.getSender();
+ String message = receivedSMSDTO.getMessage();
+ if (sender != null && !SMSFilterHelper.shouldProcessSMS(sender, message, context)) {
+ Log.d(TAG, "SMS from " + sender + " filtered out by filter rules");
+ return;
+ }
+
// Generate fingerprint for deduplication
String fingerprint = generateFingerprint(
receivedSMSDTO.getSender(),
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index aa94ff7..19bfd90 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -394,6 +394,22 @@
android:minHeight="32dp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/dialog_add_filter_rule.xml b/android/app/src/main/res/layout/dialog_add_filter_rule.xml
new file mode 100644
index 0000000..39511ab
--- /dev/null
+++ b/android/app/src/main/res/layout/dialog_add_filter_rule.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/item_filter_rule.xml b/android/app/src/main/res/layout/item_filter_rule.xml
new file mode 100644
index 0000000..8d31091
--- /dev/null
+++ b/android/app/src/main/res/layout/item_filter_rule.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+