feat(android): support filters for received sms

This commit is contained in:
isra el
2026-02-06 15:31:17 +03:00
parent ebc90d8105
commit c1952d2030
11 changed files with 1016 additions and 1 deletions

View File

@@ -81,6 +81,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.SMSFilterActivity"
android:exported="false"
android:theme="@style/Theme.SMSGateway.NoActionBar"
android:parentActivityName=".activities.MainActivity" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"

View File

@@ -22,4 +22,5 @@ public class AppConstants {
public static final String HEARTBEAT_WORK_TAG = "heartbeat";
public static final String SHARED_PREFS_HEARTBEAT_ENABLED_KEY = "HEARTBEAT_ENABLED";
public static final String SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY = "HEARTBEAT_INTERVAL_MINUTES";
public static final String SHARED_PREFS_SMS_FILTER_CONFIG_KEY = "SMS_FILTER_CONFIG";
}

View File

@@ -7,6 +7,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import com.vernu.sms.activities.SMSFilterActivity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -49,7 +50,7 @@ public class MainActivity extends AppCompatActivity {
private Context mContext;
private Switch gatewaySwitch, receiveSMSSwitch, stickyNotificationSwitch;
private EditText apiKeyEditText, fcmTokenEditText, deviceIdEditText;
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn, checkUpdatesBtn;
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn, checkUpdatesBtn, configureFilterBtn;
private ImageButton copyDeviceIdImgBtn;
private TextView deviceBrandAndModelTxt, deviceIdTxt, appVersionNameTxt, appVersionCodeTxt;
private RadioGroup defaultSimSlotRadioGroup;
@@ -81,6 +82,7 @@ public class MainActivity extends AppCompatActivity {
appVersionNameTxt = findViewById(R.id.appVersionNameTxt);
appVersionCodeTxt = findViewById(R.id.appVersionCodeTxt);
checkUpdatesBtn = findViewById(R.id.checkUpdatesBtn);
configureFilterBtn = findViewById(R.id.configureFilterBtn);
deviceIdTxt.setText(deviceId);
deviceIdEditText.setText(deviceId);
@@ -241,6 +243,11 @@ public class MainActivity extends AppCompatActivity {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(downloadUrl));
startActivity(browserIntent);
});
configureFilterBtn.setOnClickListener(view -> {
Intent filterIntent = new Intent(MainActivity.this, SMSFilterActivity.class);
startActivity(filterIntent);
});
}
private void renderAvailableSimOptions() {

View File

@@ -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<String> 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<String> 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<FilterRulesAdapter.ViewHolder> {
private List<SMSFilterRule> rules;
public FilterRulesAdapter(List<SMSFilterRule> 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);
}
}
}
}

View File

@@ -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<SMSFilterRule> 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<SMSFilterRule> getRules() {
return rules;
}
public void setRules(List<SMSFilterRule> 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<FilterConfig>() {}.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);
}
}

View File

@@ -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);
}
}

View File

@@ -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(),

View File

@@ -394,6 +394,22 @@
android:minHeight="32dp" />
</LinearLayout>
<!-- Filter Configuration Link -->
<Button
android:id="@+id/configureFilterBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Configure Filter"
android:textColor="?attr/colorPrimary"
android:textSize="15sp"
android:letterSpacing="0.01"
android:backgroundTint="@android:color/transparent"
android:drawableStart="@android:drawable/ic_menu_manage"
android:drawablePadding="6dp"
android:paddingHorizontal="0dp"
style="@style/Widget.MaterialComponents.Button.TextButton" />
<!-- Sticky Notification Setting -->
<LinearLayout
android:layout_width="match_parent"

View File

@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:background="@color/background_primary"
tools:context=".activities.SMSFilterActivity">
<!-- Header Section -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:orientation="vertical"
android:padding="28dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="10dp">
<ImageButton
android:id="@+id/backButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_revert"
android:tint="@color/white"
android:contentDescription="Back"
android:padding="8dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="SMS Filter"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:letterSpacing="0.02"
android:shadowColor="#80000000"
android:shadowDx="0"
android:shadowDy="2"
android:shadowRadius="4" />
<View
android:layout_width="48dp"
android:layout_height="48dp" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Configure SMS filtering rules"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="13sp"
android:letterSpacing="0.01"
android:alpha="0.95" />
</LinearLayout>
<!-- Scrollable Content -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/header">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Filter Settings Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/background_secondary"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="18dp"
android:text="Filter Settings"
android:textColor="@color/text_primary"
android:textSize="19sp"
android:textStyle="bold"
android:letterSpacing="0.01" />
<!-- Filter Enabled Toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enable Filter"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enable SMS filtering"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
<Switch
android:id="@+id/filterEnabledSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="32dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:background="@color/divider"
android:alpha="0.6"
android:layout_marginBottom="18dp" />
<!-- Filter Mode Selection -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="Filter Mode"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/filterModeRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp">
<RadioButton
android:id="@+id/allowListRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Allow List (only process SMS matching these filters)"
android:textColor="@color/text_primary"
android:paddingVertical="8dp" />
<RadioButton
android:id="@+id/blockListRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Block List (exclude these numbers/messages)"
android:textColor="@color/text_primary"
android:paddingVertical="8dp" />
</RadioGroup>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Filter Rules Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/background_secondary"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="18dp"
android:text="Filter Rules"
android:textColor="@color/text_primary"
android:textSize="19sp"
android:textStyle="bold"
android:letterSpacing="0.01" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/filterRulesRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/addRuleFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="16dp"
android:src="@android:drawable/ic_input_add"
app:tint="@color/white"
app:backgroundTint="?attr/colorPrimary" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/dialogTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="Add Filter Rule"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="Pattern (phone number or text)"
app:boxBackgroundColor="@android:color/transparent"
app:boxStrokeColor="?attr/colorPrimary"
app:hintTextColor="?attr/colorPrimary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/patternEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textColor="@color/text_primary"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Filter Target"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<Spinner
android:id="@+id/filterTargetSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:minHeight="48dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Match Type"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<Spinner
android:id="@+id/matchTypeSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:minHeight="48dp" />
<!-- Case Sensitivity Toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Case Sensitive"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Match pattern with exact case"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
<Switch
android:id="@+id/caseSensitiveSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="32dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="Cancel"
android:textColor="?attr/colorPrimary"
android:backgroundTint="@android:color/transparent"
style="@style/Widget.MaterialComponents.Button.TextButton" />
<Button
android:id="@+id/saveButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Save"
android:textColor="@color/button_text_color"
android:backgroundTint="@color/button_background_tint"
style="@style/Widget.MaterialComponents.Button"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="@color/background_primary"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/patternText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pattern"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/matchTypeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Match Type"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/filterTargetText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Filter Target"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<ImageButton
android:id="@+id/editButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_edit"
android:tint="?attr/colorPrimary"
android:contentDescription="Edit rule" />
<ImageButton
android:id="@+id/deleteButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_delete"
android:tint="#F44336"
android:contentDescription="Delete rule" />
</LinearLayout>
</androidx.cardview.widget.CardView>