mirror of
https://github.com/vernu/textbee.git
synced 2026-02-19 23:26:14 -05:00
feat(android): support filters for received sms
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
230
android/app/src/main/res/layout/activity_sms_filter.xml
Normal file
230
android/app/src/main/res/layout/activity_sms_filter.xml
Normal 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>
|
||||
132
android/app/src/main/res/layout/dialog_add_filter_rule.xml
Normal file
132
android/app/src/main/res/layout/dialog_add_filter_rule.xml
Normal 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>
|
||||
71
android/app/src/main/res/layout/item_filter_rule.xml
Normal file
71
android/app/src/main/res/layout/item_filter_rule.xml
Normal 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>
|
||||
Reference in New Issue
Block a user