Add local blacklist

This commit is contained in:
xynngh 2020-08-04 01:19:16 +04:00
parent 7fac34efb1
commit 636d56bdc2
47 changed files with 1717 additions and 233 deletions

View File

@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao'
android {
compileSdkVersion 29
@ -31,6 +32,10 @@ android {
}
}
greendao {
schemaVersion 1
}
dependencies {
def eventbus_version = '3.2.0'
@ -43,9 +48,11 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.work:work-runtime:2.4.0'
implementation 'org.greenrobot:greendao:3.3.0'
implementation "org.greenrobot:eventbus:$eventbus_version"
annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbus_version"
}

View File

@ -33,16 +33,34 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".BlacklistActivity"
android:label="@string/title_blacklist_activity"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
<activity
android:name=".EditBlacklistItemActivity"
android:label="@string/title_add_blacklist_item_activity"
android:parentActivityName=".BlacklistActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".BlacklistActivity" />
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_settings_activity">
android:label="@string/title_settings_activity"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
<activity
android:name=".DebugActivity"
android:label="@string/debug_activity_label">
android:label="@string/debug_activity_label"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />

View File

@ -0,0 +1,217 @@
package dummydomain.yetanothercallblocker;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.RecyclerView;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import dummydomain.yetanothercallblocker.data.BlacklistService;
import dummydomain.yetanothercallblocker.data.DatabaseSingleton;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
import dummydomain.yetanothercallblocker.event.BlacklistChangedEvent;
public class BlacklistActivity extends AppCompatActivity {
private final Settings settings = App.getSettings();
private final BlacklistDao blacklistDao = DatabaseSingleton.getBlacklistDao();
private final BlacklistService blacklistService = DatabaseSingleton.getBlacklistService();
private BlacklistItemRecyclerViewAdapter blacklistAdapter;
private SelectionTracker<Long> selectionTracker;
private ActionMode.Callback actionModeCallback;
private ActionMode actionMode;
private AsyncTask<Void, Void, List<BlacklistItem>> loadBlacklistTask;
public static Intent getIntent(Context context) {
return new Intent(context, BlacklistActivity.class);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_blacklist);
blacklistAdapter = new BlacklistItemRecyclerViewAdapter(this::onItemClicked);
RecyclerView recyclerView = findViewById(R.id.blacklistItemsList);
recyclerView.setAdapter(blacklistAdapter);
recyclerView.addItemDecoration(new CustomVerticalDivider(this));
selectionTracker = new SelectionTracker.Builder<>(
"blacklistSelection", recyclerView,
blacklistAdapter.getItemKeyProvider(),
blacklistAdapter.getItemDetailsLookup(recyclerView),
StorageStrategy.createLongStorage())
.build();
blacklistAdapter.setSelectionTracker(selectionTracker);
actionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.activity_blacklist_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.menu_delete) {
new AlertDialog.Builder(BlacklistActivity.this)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.blacklist_delete_confirmation)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (selectionTracker.hasSelection()) {
blacklistService.delete(selectionTracker.getSelection());
selectionTracker.clearSelection();
loadItems();
}
})
.setNegativeButton(R.string.no, null)
.show();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
selectionTracker.clearSelection();
actionMode = null;
}
};
selectionTracker.addObserver(new SelectionTracker.SelectionObserver<Long>() {
@Override
public void onItemStateChanged(@NonNull Long key, boolean selected) {
if (selectionTracker.hasSelection()) {
if (actionMode == null) {
actionMode = startSupportActionMode(actionModeCallback);
}
} else {
if (actionMode != null) {
actionMode.finish();
actionMode = null;
}
}
if (actionMode != null) {
actionMode.setTitle(getString(R.string.selected_count,
selectionTracker.getSelection().size()));
}
}
});
selectionTracker.onRestoreInstanceState(savedInstanceState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_blacklist, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.menu_block_blacklisted).setChecked(
settings.getBlockBlacklisted());
return super.onPrepareOptionsMenu(menu);
}
@Override
protected void onStart() {
super.onStart();
EventUtils.register(this);
loadItems();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
selectionTracker.onSaveInstanceState(outState);
}
@Override
protected void onStop() {
EventUtils.unregister(this);
super.onStop();
}
@Override
protected void onDestroy() {
cancelLoadingBlacklistTask();
super.onDestroy();
}
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void onBlacklistChanged(BlacklistChangedEvent blacklistChangedEvent) {
loadItems();
}
private void loadItems() {
cancelLoadingBlacklistTask();
@SuppressLint("StaticFieldLeak")
AsyncTask<Void, Void, List<BlacklistItem>> loadBlacklistTask = this.loadBlacklistTask
= new AsyncTask<Void, Void, List<BlacklistItem>>() {
@Override
protected List<BlacklistItem> doInBackground(Void... voids) {
return blacklistDao.detach(blacklistDao.loadAll());
}
@Override
protected void onPostExecute(List<BlacklistItem> items) {
blacklistAdapter.setItems(items);
}
};
loadBlacklistTask.execute();
}
private void cancelLoadingBlacklistTask() {
if (loadBlacklistTask != null) {
loadBlacklistTask.cancel(true);
loadBlacklistTask = null;
}
}
public void onBlockBlacklistedChanged(MenuItem item) {
settings.setBlockBlacklisted(!item.isChecked());
}
public void onAddClicked(View view) {
startActivity(EditBlacklistItemActivity.getIntent(this, null, null));
}
private void onItemClicked(BlacklistItem blacklistItem) {
startActivity(EditBlacklistItemActivity.getIntent(this, blacklistItem.getId()));
}
}

View File

@ -0,0 +1,193 @@
package dummydomain.yetanothercallblocker;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.util.ObjectsCompat;
import androidx.recyclerview.selection.ItemDetailsLookup;
import androidx.recyclerview.selection.ItemKeyProvider;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.RecyclerView;
import java.text.DateFormat;
import java.util.List;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
public class BlacklistItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
<BlacklistItem, BlacklistItemRecyclerViewAdapter.ViewHolder> {
private SelectionTracker<Long> selectionTracker;
public BlacklistItemRecyclerViewAdapter(
@Nullable ListInteractionListener<BlacklistItem> listener) {
super(listener);
}
public void setSelectionTracker(SelectionTracker<Long> selectionTracker) {
this.selectionTracker = selectionTracker;
}
public ItemKeyProvider<Long> getItemKeyProvider() {
return new ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {
@Nullable
@Override
public Long getKey(int position) {
return items.get(position).getId();
}
@Override
public int getPosition(@NonNull Long key) {
for (int i = 0; i < items.size(); i++) {
BlacklistItem item = items.get(i);
if (key.equals(item.getId())) return i;
}
return RecyclerView.NO_POSITION;
}
};
}
public ItemDetailsLookup<Long> getItemDetailsLookup(RecyclerView recyclerView) {
return new ItemDetailsLookup<Long>() {
@Nullable
@Override
public ItemDetails<Long> getItemDetails(@NonNull MotionEvent e) {
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view);
if (holder instanceof BlacklistItemRecyclerViewAdapter.ViewHolder) {
return ((BlacklistItemRecyclerViewAdapter.ViewHolder) holder).getItemDetails();
}
}
return null;
}
};
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.blacklist_item, parent, false);
return new BlacklistItemRecyclerViewAdapter.ViewHolder(view);
}
@Override
protected DiffUtilCallback getDiffUtilCallback(
List<BlacklistItem> oldList, List<BlacklistItem> newList) {
return new DiffUtilCallback(oldList, newList);
}
class ViewHolder extends GenericRecyclerViewAdapter
<BlacklistItem, BlacklistItemRecyclerViewAdapter.ViewHolder>.GenericViewHolder {
final TextView name, pattern, stats;
final AppCompatImageView errorIcon;
final DateFormat dateFormat, timeFormat;
ItemDetailsLookup.ItemDetails<Long> itemDetails;
public ViewHolder(@NonNull View itemView) {
super(itemView);
name = itemView.findViewById(R.id.name);
pattern = itemView.findViewById(R.id.pattern);
stats = itemView.findViewById(R.id.stats);
errorIcon = itemView.findViewById(R.id.errorIcon);
dateFormat = android.text.format.DateFormat.getMediumDateFormat(itemView.getContext());
timeFormat = android.text.format.DateFormat.getTimeFormat(itemView.getContext());
}
@Override
void bind(BlacklistItem item) {
name.setText(item.getName());
name.setVisibility(TextUtils.isEmpty(item.getName()) ? View.GONE : View.VISIBLE);
pattern.setText(item.getHumanReadablePattern());
if (item.getNumberOfCalls() > 0) {
stats.setVisibility(View.VISIBLE);
Context context = stats.getContext();
String dateString = item.getLastCallDate() != null
? dateFormat.format(item.getLastCallDate()) + ' '
+ timeFormat.format(item.getLastCallDate())
: context.getString(R.string.blacklist_item_date_no_info);
stats.setText(context.getResources().getQuantityString(
R.plurals.blacklist_item_stats, item.getNumberOfCalls(),
item.getNumberOfCalls(), dateString));
} else {
stats.setVisibility(View.GONE);
}
errorIcon.setVisibility(item.getInvalid() ? View.VISIBLE : View.GONE);
if (selectionTracker != null) {
itemView.setActivated(selectionTracker.isSelected(item.getId()));
}
}
ItemDetailsLookup.ItemDetails<Long> getItemDetails() {
if (itemDetails == null) {
itemDetails = new ItemDetailsLookup.ItemDetails<Long>() {
@Override
public int getPosition() {
return getAdapterPosition();
}
@Nullable
@Override
public Long getSelectionKey() {
int position = getAdapterPosition();
return position != RecyclerView.NO_POSITION
? items.get(position).getId() : null;
}
};
}
return itemDetails;
}
@Override
public String toString() {
return super.toString() + " '" + pattern.getText() + "'";
}
}
static class DiffUtilCallback
extends GenericRecyclerViewAdapter.GenericDiffUtilCallback<BlacklistItem> {
DiffUtilCallback(List<BlacklistItem> oldList, List<BlacklistItem> newList) {
super(oldList, newList);
}
@Override
protected boolean areItemsTheSame(BlacklistItem oldItem, BlacklistItem newItem) {
if (oldItem.getId() != null || newItem.getId() != null) {
return ObjectsCompat.equals(oldItem.getId(), newItem.getId());
}
return ObjectsCompat.equals(oldItem.getPattern(), newItem.getPattern());
}
@Override
protected boolean areContentsTheSame(BlacklistItem oldItem, BlacklistItem newItem) {
return ObjectsCompat.equals(oldItem.getPattern(), newItem.getPattern())
&& ObjectsCompat.equals(oldItem.getName(), newItem.getName())
&& oldItem.getNumberOfCalls() == newItem.getNumberOfCalls()
&& ObjectsCompat.equals(oldItem.getLastCallDate(), newItem.getLastCallDate());
}
}
}

View File

@ -10,63 +10,16 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Collections;
import java.util.List;
import dummydomain.yetanothercallblocker.data.CallLogItem;
public class CallLogItemRecyclerViewAdapter
extends RecyclerView.Adapter<CallLogItemRecyclerViewAdapter.ViewHolder> {
public class CallLogItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
<CallLogItem, CallLogItemRecyclerViewAdapter.ViewHolder> {
public interface OnListInteractionListener {
void onListFragmentInteraction(CallLogItem item);
}
private static class DiffUtilCallback extends DiffUtil.Callback {
private List<CallLogItem> oldList;
private List<CallLogItem> newList;
DiffUtilCallback(List<CallLogItem> oldList, List<CallLogItem> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
CallLogItem oldItem = oldList.get(oldItemPosition);
CallLogItem newItem = newList.get(newItemPosition);
return newItem.type == oldItem.type
&& TextUtils.equals(newItem.number, oldItem.number)
&& newItem.timestamp == oldItem.timestamp
&& newItem.duration == oldItem.duration;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return false; // time always updates
}
}
private final @Nullable OnListInteractionListener listener;
private List<CallLogItem> items = Collections.emptyList();
public CallLogItemRecyclerViewAdapter(@Nullable OnListInteractionListener listener) {
this.listener = listener;
public CallLogItemRecyclerViewAdapter(@Nullable ListInteractionListener<CallLogItem> listener) {
super(listener);
}
@Override
@ -78,32 +31,12 @@ public class CallLogItemRecyclerViewAdapter
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
holder.bind(items.get(position));
protected DiffUtilCallback getDiffUtilCallback(
List<CallLogItem> oldList, List<CallLogItem> newList) {
return new DiffUtilCallback(oldList, newList);
}
@Override
public int getItemCount() {
return items.size();
}
public void setItems(List<CallLogItem> items) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new DiffUtilCallback(this.items, items));
this.items = items;
diffResult.dispatchUpdatesTo(this);
}
private void onClick(int index) {
if (index != RecyclerView.NO_POSITION && listener != null) {
listener.onListFragmentInteraction(items.get(index));
}
}
class ViewHolder extends RecyclerView.ViewHolder {
final View view;
class ViewHolder extends GenericRecyclerViewAdapter<CallLogItem, ViewHolder>.GenericViewHolder {
final AppCompatImageView callTypeIcon;
final TextView label;
@ -113,16 +46,13 @@ public class CallLogItemRecyclerViewAdapter
ViewHolder(View view) {
super(view);
this.view = view;
callTypeIcon = view.findViewById(R.id.callTypeIcon);
label = view.findViewById(R.id.item_label);
numberInfoIcon = view.findViewById(R.id.numberInfoIcon);
time = view.findViewById(R.id.time);
view.setOnClickListener(v -> onClick(getAdapterPosition()));
}
@Override
void bind(CallLogItem item) {
Integer icon;
switch (item.type) {
@ -180,4 +110,27 @@ public class CallLogItemRecyclerViewAdapter
return super.toString() + " '" + label.getText() + "'";
}
}
static class DiffUtilCallback
extends GenericRecyclerViewAdapter.GenericDiffUtilCallback<CallLogItem> {
DiffUtilCallback(List<CallLogItem> oldList, List<CallLogItem> newList) {
super(oldList, newList);
}
@Override
protected boolean areItemsTheSame(CallLogItem oldItem, CallLogItem newItem) {
return newItem.type == oldItem.type
&& TextUtils.equals(newItem.number, oldItem.number)
&& newItem.timestamp == oldItem.timestamp
&& newItem.duration == oldItem.duration;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return false; // time always updates
}
}
}

View File

@ -20,6 +20,7 @@ import java.lang.reflect.Method;
import dummydomain.yetanothercallblocker.data.DatabaseSingleton;
import dummydomain.yetanothercallblocker.data.NumberInfo;
import dummydomain.yetanothercallblocker.data.NumberInfoService;
import dummydomain.yetanothercallblocker.event.CallEndedEvent;
import dummydomain.yetanothercallblocker.event.CallOngoingEvent;
@ -66,14 +67,18 @@ public class CallReceiver extends BroadcastReceiver {
boolean showNotifications = settings.getIncomingCallNotifications();
if (blockingEnabled || showNotifications) {
NumberInfo numberInfo = DatabaseSingleton.getNumberInfo(incomingNumber);
NumberInfoService numberInfoService = DatabaseSingleton.getNumberInfoService();
NumberInfo numberInfo = numberInfoService.getNumberInfo(incomingNumber, false);
boolean blocked = false;
if (blockingEnabled && !isOnCall && DatabaseSingleton.shouldBlock(numberInfo)) {
if (blockingEnabled && !isOnCall && numberInfoService.shouldBlock(numberInfo)) {
blocked = rejectCall(context);
if (blocked) {
NotificationHelper.showBlockedCallNotification(context, numberInfo);
numberInfoService.blockedCall(numberInfo);
postEvent(new CallEndedEvent());
}
}

View File

@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory;
import dummydomain.yetanothercallblocker.data.DatabaseSingleton;
import dummydomain.yetanothercallblocker.data.NumberInfo;
import dummydomain.yetanothercallblocker.data.NumberInfoService;
import dummydomain.yetanothercallblocker.event.CallEndedEvent;
import static dummydomain.yetanothercallblocker.EventUtils.postEvent;
@ -28,6 +29,8 @@ public class CallScreeningServiceImpl extends CallScreeningService {
private static final Logger LOG = LoggerFactory.getLogger(CallScreeningServiceImpl.class);
private NumberInfoService numberInfoService = DatabaseSingleton.getNumberInfoService();
@Override
public void onScreenCall(@NonNull Call.Details callDetails) {
LOG.info("onScreenCall({})", callDetails);
@ -95,9 +98,9 @@ public class CallScreeningServiceImpl extends CallScreeningService {
}
if (!ignore) {
numberInfo = DatabaseSingleton.getNumberInfo(number);
numberInfo = numberInfoService.getNumberInfo(number, false);
shouldBlock = DatabaseSingleton.shouldBlock(numberInfo);
shouldBlock = numberInfoService.shouldBlock(numberInfo);
}
} finally {
LOG.debug("onScreenCall() blocking call: {}", shouldBlock);
@ -124,9 +127,13 @@ public class CallScreeningServiceImpl extends CallScreeningService {
NotificationHelper.showBlockedCallNotification(this, numberInfo);
numberInfoService.blockedCall(numberInfo);
postEvent(new CallEndedEvent());
}
}
LOG.debug("onScreenCall() finished");
}
private void extraLogging(Call.Details callDetails) {

View File

@ -0,0 +1,214 @@
package dummydomain.yetanothercallblocker;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Objects;
import dummydomain.yetanothercallblocker.data.BlacklistService;
import dummydomain.yetanothercallblocker.data.BlacklistUtils;
import dummydomain.yetanothercallblocker.data.DatabaseSingleton;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
import static dummydomain.yetanothercallblocker.data.BlacklistUtils.cleanPattern;
import static dummydomain.yetanothercallblocker.data.BlacklistUtils.patternFromHumanReadable;
public class EditBlacklistItemActivity extends AppCompatActivity {
private static final String PARAM_ITEM_ID = "itemId";
private static final String PARAM_NAME = "itemName";
private static final String PARAM_NUMBER_PATTERN = "numberPattern";
private static final Logger LOG = LoggerFactory.getLogger(EditBlacklistItemActivity.class);
private BlacklistDao blacklistDao = DatabaseSingleton.getBlacklistDao();
private BlacklistService blacklistService = DatabaseSingleton.getBlacklistService();
private TextInputLayout nameTextField;
private TextInputLayout patternTextField;
private BlacklistItem blacklistItem;
public static Intent getIntent(Context context, long itemId) {
Intent intent = new Intent(context, EditBlacklistItemActivity.class);
intent.putExtra(PARAM_ITEM_ID, itemId);
return intent;
}
public static Intent getIntent(Context context, String name, String numberPattern) {
Intent intent = new Intent(context, EditBlacklistItemActivity.class);
intent.putExtra(PARAM_NAME, name);
intent.putExtra(PARAM_NUMBER_PATTERN, numberPattern);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_blacklist_item);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
nameTextField = findViewById(R.id.nameTextField);
patternTextField = findViewById(R.id.patternTextField);
EditText patternEditText = Objects.requireNonNull(patternTextField.getEditText());
patternEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
validate();
}
});
patternEditText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onSaveClicked(null);
return true;
}
return false;
}
);
long itemIdFromParams = getIntent().getLongExtra(PARAM_ITEM_ID, -1);
if (itemIdFromParams != -1) {
blacklistItem = blacklistDao.findById(itemIdFromParams);
if (blacklistItem == null) {
LOG.warn("onCreate() no item with id={}", itemIdFromParams);
finish();
return;
}
setTitle(R.string.title_edit_blacklist_item_activity);
}
if (savedInstanceState == null) {
String name;
String pattern;
if (blacklistItem != null) {
name = blacklistItem.getName();
pattern = blacklistItem.getPattern();
} else {
name = getIntent().getStringExtra(PARAM_NAME);
pattern = getIntent().getStringExtra(PARAM_NUMBER_PATTERN);
}
if (!TextUtils.isEmpty(pattern)) {
pattern = BlacklistUtils.patternToHumanReadable(pattern);
}
setString(nameTextField, name);
setString(patternTextField, pattern);
}
patternTextField.requestFocus();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_edit_blacklist_item, menu);
if (blacklistItem == null) {
menu.findItem(R.id.menu_delete).setVisible(false);
}
return true;
}
public void onSaveClicked(MenuItem item) {
if (validate()) {
save();
finish();
}
}
public void onDeleteClicked(MenuItem item) {
blacklistService.delete(Collections.singletonList(blacklistItem.getId()));
finish();
}
private boolean validate() {
String pattern = getString(patternTextField);
boolean valid = true;
boolean empty = TextUtils.isEmpty(pattern);
if (blacklistItem != null || !empty) {
pattern = cleanPattern(patternFromHumanReadable(pattern));
valid = BlacklistUtils.isValidPattern(pattern);
}
patternTextField.setError(!valid ? getString(
empty ? R.string.number_pattern_empty : R.string.number_pattern_incorrect)
: null);
return valid;
}
private void save() {
String name = getString(nameTextField);
String pattern = cleanPattern(patternFromHumanReadable(getString(patternTextField)));
boolean invalid = !BlacklistUtils.isValidPattern(pattern);
if (blacklistItem != null) {
boolean changed = false;
if (!TextUtils.equals(name, blacklistItem.getName())) {
blacklistItem.setName(name);
changed = true;
}
if (!TextUtils.equals(pattern, blacklistItem.getPattern())) {
blacklistItem.setPattern(pattern);
changed = true;
}
if (invalid != blacklistItem.getInvalid()) {
changed = true;
}
if (changed) {
blacklistService.save(blacklistItem);
}
} else {
if (TextUtils.isEmpty(name) && TextUtils.isEmpty(pattern)) {
LOG.warn("save() not creating a new item because fields are empty");
return;
}
BlacklistItem blacklistItem = new BlacklistItem(name, pattern);
blacklistService.save(blacklistItem);
}
}
private String getString(TextInputLayout textInputLayout) {
return Objects.requireNonNull(textInputLayout.getEditText()).getText().toString();
}
private void setString(TextInputLayout textInputLayout, String s) {
Objects.requireNonNull(textInputLayout.getEditText()).setText(s);
}
}

View File

@ -0,0 +1,109 @@
package dummydomain.yetanothercallblocker;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Collections;
import java.util.List;
public abstract class GenericRecyclerViewAdapter<T, V extends GenericRecyclerViewAdapter<T, V>.GenericViewHolder>
extends RecyclerView.Adapter<V> {
public interface ListInteractionListener<T> {
void onListItemClicked(T item);
}
protected @Nullable
ListInteractionListener<T> listener;
protected List<T> items = Collections.emptyList();
public GenericRecyclerViewAdapter(@Nullable ListInteractionListener<T> listener) {
this.listener = listener;
}
@Override
public void onBindViewHolder(@NonNull V holder, int position) {
onBindViewHolder(holder, items.get(position));
}
protected void onBindViewHolder(@NonNull V holder, T item) {
holder.bind(item);
}
@Override
public int getItemCount() {
return items.size();
}
public void setItems(List<T> items) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
getDiffUtilCallback(this.items, items));
this.items = items;
diffResult.dispatchUpdatesTo(this);
}
protected abstract GenericDiffUtilCallback<T> getDiffUtilCallback(
List<T> oldList, List<T> newList);
protected void onClick(int index) {
if (index != RecyclerView.NO_POSITION && listener != null) {
listener.onListItemClicked(items.get(index));
}
}
protected static class GenericDiffUtilCallback<T> extends DiffUtil.Callback {
protected List<T> oldList;
protected List<T> newList;
protected GenericDiffUtilCallback(List<T> oldList, List<T> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsTheSame(oldList.get(oldItemPosition), newList.get(newItemPosition));
}
protected boolean areItemsTheSame(T oldItem, T newItem) {
return false;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return areContentsTheSame(oldList.get(oldItemPosition), newList.get(newItemPosition));
}
protected boolean areContentsTheSame(T oldItem, T newItem) {
return false;
}
}
protected abstract class GenericViewHolder extends RecyclerView.ViewHolder {
public GenericViewHolder(@NonNull View itemView) {
super(itemView);
itemView.setOnClickListener(v -> onClick(getAdapterPosition()));
}
abstract void bind(T item);
}
}

View File

@ -97,13 +97,13 @@ public class InfoDialogHelper {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
if (numberInfo.contactItem != null) {
new AlertDialog.Builder(context)
.setTitle(R.string.load_reviews_confirmation_title)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.load_reviews_confirmation_message)
.setPositiveButton(android.R.string.yes, (d1, w) -> {
.setPositiveButton(R.string.yes, (d1, w) -> {
reviewsAction.run();
dialog.dismiss();
})
.setNegativeButton(android.R.string.no, null)
.setNegativeButton(R.string.no, null)
.show();
} else {
reviewsAction.run();
@ -114,13 +114,13 @@ public class InfoDialogHelper {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
if (numberInfo.contactItem != null) {
new AlertDialog.Builder(context)
.setTitle(R.string.load_reviews_confirmation_title)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.load_reviews_confirmation_message)
.setPositiveButton(android.R.string.yes, (d1, w) -> {
.setPositiveButton(R.string.yes, (d1, w) -> {
webReviewAction.run();
dialog.dismiss();
})
.setNegativeButton(android.R.string.no, null)
.setNegativeButton(R.string.no, null)
.show();
} else {
webReviewAction.run();

View File

@ -200,6 +200,10 @@ public class MainActivity extends AppCompatActivity {
loadCallLog();
}
public void onOpenBlacklist(MenuItem item) {
startActivity(BlacklistActivity.getIntent(this));
}
public void onOpenSettings(MenuItem item) {
startActivity(new Intent(this, SettingsActivity.class));
}

View File

@ -16,6 +16,8 @@ public class Settings extends GenericSettings {
public static final String PREF_INCOMING_CALL_NOTIFICATIONS = "incomingCallNotifications";
public static final String PREF_BLOCK_NEGATIVE_SIA_NUMBERS = "blockNegativeSiaNumbers";
public static final String PREF_BLOCK_HIDDEN_NUMBERS = "blockHiddenNumbers";
public static final String PREF_BLOCK_BLACKLISTED = "blockBlacklisted";
public static final String PREF_BLACKLIST_IS_NOT_EMPTY = "blacklistIsNotEmpty";
public static final String PREF_USE_CONTACTS = "useContacts";
public static final String PREF_UI_MODE = "uiMode";
public static final String PREF_NUMBER_OF_RECENT_CALLS = "numberOfRecentCalls";
@ -94,7 +96,7 @@ public class Settings extends GenericSettings {
}
public boolean getCallBlockingEnabled() {
return getBlockNegativeSiaNumbers() || getBlockHiddenNumbers();
return getBlockNegativeSiaNumbers() || getBlockHiddenNumbers() || getBlacklistEnabled();
}
public boolean getBlockNegativeSiaNumbers() {
@ -113,6 +115,26 @@ public class Settings extends GenericSettings {
setBoolean(PREF_BLOCK_HIDDEN_NUMBERS, block);
}
public boolean getBlacklistEnabled() {
return getBlockBlacklisted() && getBlacklistIsNotEmpty();
}
public boolean getBlockBlacklisted() {
return getBoolean(PREF_BLOCK_BLACKLISTED, true);
}
public void setBlockBlacklisted(boolean block) {
setBoolean(PREF_BLOCK_BLACKLISTED, block);
}
public boolean getBlacklistIsNotEmpty() {
return getBoolean(PREF_BLACKLIST_IS_NOT_EMPTY);
}
public void setBlacklistIsNotEmpty(boolean flag) {
setBoolean(PREF_BLACKLIST_IS_NOT_EMPTY, flag);
}
public boolean getUseContacts() {
return getBoolean(PREF_USE_CONTACTS);
}

View File

@ -156,6 +156,8 @@ public class SettingsActivity extends AppCompatActivity
.setOnPreferenceChangeListener(callBlockingListener);
requireNonNull((SwitchPreferenceCompat) findPreference(Settings.PREF_BLOCK_HIDDEN_NUMBERS))
.setOnPreferenceChangeListener(callBlockingListener);
requireNonNull((SwitchPreferenceCompat) findPreference(Settings.PREF_BLOCK_BLACKLISTED))
.setOnPreferenceChangeListener(callBlockingListener);
SwitchPreferenceCompat callScreeningPref =
requireNonNull(findPreference(PREF_USE_CALL_SCREENING_SERVICE));

View File

@ -0,0 +1,66 @@
package dummydomain.yetanothercallblocker.data;
import android.text.TextUtils;
import java.util.Date;
import java.util.Objects;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
import dummydomain.yetanothercallblocker.event.BlacklistChangedEvent;
import dummydomain.yetanothercallblocker.event.BlacklistItemChangedEvent;
import static dummydomain.yetanothercallblocker.EventUtils.postEvent;
public class BlacklistService {
public interface Callback {
void changed(boolean notEmpty);
}
private final Callback callback;
private final BlacklistDao blacklistDao;
public BlacklistService(Callback callback, BlacklistDao blacklistDao) {
this.callback = callback;
this.blacklistDao = blacklistDao;
}
public BlacklistItem getBlacklistItemForNumber(String number) {
if (TextUtils.isEmpty(number)) return null;
number = BlacklistUtils.cleanNumber(number);
return blacklistDao.getFirstMatch(number);
}
public void save(BlacklistItem blacklistItem) {
boolean newItem = blacklistItem.getId() == null;
blacklistItem.setInvalid(!BlacklistUtils.isValidPattern(blacklistItem.getPattern()));
blacklistDao.save(blacklistItem);
blacklistChanged();
postEvent(newItem ? new BlacklistChangedEvent() : new BlacklistItemChangedEvent());
}
public void addCall(BlacklistItem blacklistItem, Date date) {
blacklistItem.setLastCallDate(Objects.requireNonNull(date));
blacklistItem.setNumberOfCalls(blacklistItem.getNumberOfCalls() + 1);
blacklistDao.save(blacklistItem);
postEvent(new BlacklistItemChangedEvent());
}
public void delete(Iterable<Long> keys) {
blacklistDao.delete(keys);
blacklistChanged();
}
private void blacklistChanged() {
callback.changed(blacklistDao.countValid() != 0);
}
}

View File

@ -0,0 +1,31 @@
package dummydomain.yetanothercallblocker.data;
import java.util.regex.Pattern;
public class BlacklistUtils {
private static final Pattern BLACKLIST_ITEM_VALID_PATTERN = Pattern.compile("\\+?[0-9%_]+");
private static final Pattern PATTERN_CLEANING_PATTERN = Pattern.compile("[^+0-9%_*#]");
private static final Pattern NUMBER_CLEANING_PATTERN = Pattern.compile("[^+0-9]");
public static String patternToHumanReadable(String pattern) {
return pattern.replace('%', '*').replace('_', '#');
}
public static String patternFromHumanReadable(String pattern) {
return pattern.replace('*', '%').replace('#', '_');
}
public static String cleanPattern(String pattern) {
return PATTERN_CLEANING_PATTERN.matcher(pattern).replaceAll("");
}
public static String cleanNumber(String number) {
return NUMBER_CLEANING_PATTERN.matcher(number).replaceAll("");
}
public static boolean isValidPattern(String pattern) {
return BLACKLIST_ITEM_VALID_PATTERN.matcher(pattern).matches();
}
}

View File

@ -1,29 +0,0 @@
package dummydomain.yetanothercallblocker.data;
import dummydomain.yetanothercallblocker.Settings;
public class BlockingDecisionMaker implements DatabaseSingleton.BlockingDecisionMaker {
private final Settings settings;
public BlockingDecisionMaker(Settings settings) {
this.settings = settings;
}
@Override
public boolean shouldBlock(NumberInfo numberInfo) {
if (numberInfo.contactItem != null) return false;
if (numberInfo.isHiddenNumber && settings.getBlockHiddenNumbers()) {
return true;
}
if (numberInfo.rating == NumberInfo.Rating.NEGATIVE
&& settings.getBlockNegativeSiaNumbers()) {
return true;
}
return false;
}
}

View File

@ -6,6 +6,8 @@ import android.text.TextUtils;
import java.util.concurrent.TimeUnit;
import dummydomain.yetanothercallblocker.PermissionHelper;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.data.db.YacbDaoSessionFactory;
import dummydomain.yetanothercallblocker.sia.Settings;
import dummydomain.yetanothercallblocker.sia.SettingsImpl;
import dummydomain.yetanothercallblocker.sia.Storage;
@ -104,8 +106,6 @@ public class Config {
DatabaseSingleton.setDbManager(new DbManager(storage, SIA_PATH_PREFIX,
new DbDownloader(okHttpClientFactory)));
DatabaseSingleton.setHiddenNumberDetector(NumberUtils::isHiddenNumber);
CommunityDatabase communityDatabase = new CommunityDatabase(
storage, AbstractDatabase.Source.ANY, SIA_PATH_PREFIX,
SIA_SECONDARY_PATH_PREFIX, siaSettings, webService);
@ -119,20 +119,33 @@ public class Config {
wsParameterProvider.setSiaMetadata(siaMetadata);
DatabaseSingleton.setSiaMetadata(siaMetadata);
DatabaseSingleton.setFeaturedDatabase(new FeaturedDatabase(
storage, AbstractDatabase.Source.ANY, SIA_PATH_PREFIX));
FeaturedDatabase featuredDatabase = new FeaturedDatabase(
storage, AbstractDatabase.Source.ANY, SIA_PATH_PREFIX);
DatabaseSingleton.setFeaturedDatabase(featuredDatabase);
DatabaseSingleton.setCommunityReviewsLoader(new CommunityReviewsLoader(webService));
DatabaseSingleton.setContactsProvider(number -> {
YacbDaoSessionFactory daoSessionFactory = new YacbDaoSessionFactory(context, "YACB");
BlacklistDao blacklistDao = new BlacklistDao(daoSessionFactory::getDaoSession);
DatabaseSingleton.setBlacklistDao(blacklistDao);
BlacklistService blacklistService = new BlacklistService(
settings::setBlacklistIsNotEmpty, blacklistDao);
DatabaseSingleton.setBlacklistService(blacklistService);
ContactsProvider contactsProvider = number -> {
if (settings.getUseContacts() && PermissionHelper.hasContactsPermission(context)) {
String contactName = ContactsHelper.getContactName(context, number);
if (!TextUtils.isEmpty(contactName)) return new ContactItem(contactName);
}
return null;
});
};
DatabaseSingleton.setBlockingDecisionMaker(new BlockingDecisionMaker(settings));
NumberInfoService numberInfoService = new NumberInfoService(
settings, NumberUtils::isHiddenNumber,
communityDatabase, featuredDatabase, contactsProvider, blacklistService);
DatabaseSingleton.setNumberInfoService(numberInfoService);
}
}

View File

@ -1,40 +1,26 @@
package dummydomain.yetanothercallblocker.data;
import android.text.TextUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.sia.model.CommunityReviewsLoader;
import dummydomain.yetanothercallblocker.sia.model.SiaMetadata;
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabase;
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabaseItem;
import dummydomain.yetanothercallblocker.sia.model.database.DbManager;
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabase;
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabaseItem;
import dummydomain.yetanothercallblocker.sia.network.WebService;
public class DatabaseSingleton {
private static final Logger LOG = LoggerFactory.getLogger(DatabaseSingleton.class);
private static WebService webService;
private static DbManager dbManager;
private static SiaMetadata siaMetadata;
private static HiddenNumberDetector hiddenNumberDetector;
private static CommunityDatabase communityDatabase;
private static FeaturedDatabase featuredDatabase;
private static CommunityReviewsLoader communityReviewsLoader;
private static ContactsProvider contactsProvider;
private static BlacklistDao blacklistDao;
private static BlacklistService blacklistService;
private static BlockingDecisionMaker blockingDecisionMaker;
private static NumberInfoService numberInfoService;
static void setWebService(WebService webService) {
DatabaseSingleton.webService = webService;
@ -48,10 +34,6 @@ public class DatabaseSingleton {
DatabaseSingleton.siaMetadata = siaMetadata;
}
static void setHiddenNumberDetector(HiddenNumberDetector hiddenNumberDetector) {
DatabaseSingleton.hiddenNumberDetector = hiddenNumberDetector;
}
static void setCommunityDatabase(CommunityDatabase communityDatabase) {
DatabaseSingleton.communityDatabase = communityDatabase;
}
@ -64,12 +46,16 @@ public class DatabaseSingleton {
DatabaseSingleton.communityReviewsLoader = communityReviewsLoader;
}
static void setContactsProvider(ContactsProvider contactsProvider) {
DatabaseSingleton.contactsProvider = contactsProvider;
static void setBlacklistDao(BlacklistDao blacklistDao) {
DatabaseSingleton.blacklistDao = blacklistDao;
}
static void setBlockingDecisionMaker(BlockingDecisionMaker blockingDecisionMaker) {
DatabaseSingleton.blockingDecisionMaker = blockingDecisionMaker;
static void setBlacklistService(BlacklistService blacklistService) {
DatabaseSingleton.blacklistService = blacklistService;
}
static void setNumberInfoService(NumberInfoService numberInfoService) {
DatabaseSingleton.numberInfoService = numberInfoService;
}
public static WebService getWebService() {
@ -96,80 +82,20 @@ public class DatabaseSingleton {
return communityReviewsLoader;
}
public static BlacklistDao getBlacklistDao() {
return blacklistDao;
}
public static BlacklistService getBlacklistService() {
return blacklistService;
}
public static NumberInfoService getNumberInfoService() {
return numberInfoService;
}
public static NumberInfo getNumberInfo(String number) {
LOG.debug("getNumberInfo({}) started", number);
// TODO: check number format
NumberInfo numberInfo = new NumberInfo();
numberInfo.number = number;
if (hiddenNumberDetector != null) {
numberInfo.isHiddenNumber = hiddenNumberDetector.isHiddenNumber(number);
}
LOG.trace("getNumberInfo() isHiddenNumber={}", numberInfo.isHiddenNumber);
if (numberInfo.isHiddenNumber || TextUtils.isEmpty(numberInfo.number)) {
numberInfo.noNumber = true;
}
LOG.trace("getNumberInfo() noNumber={}", numberInfo.noNumber);
if (numberInfo.noNumber) {
LOG.debug("getNumberInfo() finished");
return numberInfo;
}
numberInfo.communityDatabaseItem = DatabaseSingleton.getCommunityDatabase()
.getDbItemByNumber(number);
LOG.trace("getNumberInfo() communityItem={}", numberInfo.communityDatabaseItem);
numberInfo.featuredDatabaseItem = DatabaseSingleton.getFeaturedDatabase()
.getDbItemByNumber(number);
LOG.trace("getNumberInfo() featuredItem={}", numberInfo.featuredDatabaseItem);
numberInfo.contactItem = DatabaseSingleton.contactsProvider.get(number);
LOG.trace("getNumberInfo() contactItem={}", numberInfo.contactItem);
ContactItem contactItem = numberInfo.contactItem;
FeaturedDatabaseItem featuredItem = numberInfo.featuredDatabaseItem;
if (contactItem != null && !TextUtils.isEmpty(contactItem.displayName)) {
numberInfo.name = contactItem.displayName;
} else if (featuredItem != null && !TextUtils.isEmpty(featuredItem.getName())) {
numberInfo.name = featuredItem.getName();
}
LOG.trace("getNumberInfo() name={}", numberInfo.name);
CommunityDatabaseItem communityItem = numberInfo.communityDatabaseItem;
if (communityItem != null && communityItem.hasRatings()) {
if (communityItem.getNegativeRatingsCount() > communityItem.getPositiveRatingsCount()
+ communityItem.getNeutralRatingsCount()) {
numberInfo.rating = NumberInfo.Rating.NEGATIVE;
} else if (communityItem.getPositiveRatingsCount() > communityItem.getNeutralRatingsCount()
+ communityItem.getNegativeRatingsCount()) {
numberInfo.rating = NumberInfo.Rating.POSITIVE;
} else if (communityItem.getNeutralRatingsCount() > communityItem.getPositiveRatingsCount()
+ communityItem.getNegativeRatingsCount()) {
numberInfo.rating = NumberInfo.Rating.NEUTRAL;
}
}
LOG.trace("getNumberInfo() rating={}", numberInfo.rating);
LOG.debug("getNumberInfo() finished");
return numberInfo;
}
public static boolean shouldBlock(NumberInfo numberInfo) {
if (blockingDecisionMaker != null) {
return blockingDecisionMaker.shouldBlock(numberInfo);
}
return false;
}
public interface HiddenNumberDetector {
boolean isHiddenNumber(String number);
}
public interface BlockingDecisionMaker {
boolean shouldBlock(NumberInfo numberInfo);
return numberInfoService.getNumberInfo(number, true);
}
}

View File

@ -1,10 +1,15 @@
package dummydomain.yetanothercallblocker.data;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabaseItem;
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabaseItem;
public class NumberInfo {
public enum BlockingReason {
HIDDEN_NUMBER, SIA_RATING, BLACKLISTED
}
public enum Rating {
POSITIVE, NEUTRAL, NEGATIVE, UNKNOWN
}
@ -14,9 +19,10 @@ public class NumberInfo {
// info from various sources
public boolean isHiddenNumber;
public ContactItem contactItem;
public CommunityDatabaseItem communityDatabaseItem;
public FeaturedDatabaseItem featuredDatabaseItem;
public ContactItem contactItem;
public BlacklistItem blacklistItem;
// computed rating
public Rating rating = Rating.UNKNOWN;
@ -24,5 +30,6 @@ public class NumberInfo {
// precomputed for convenience
public boolean noNumber;
public String name;
public BlockingReason blockingReason;
}

View File

@ -0,0 +1,152 @@
package dummydomain.yetanothercallblocker.data;
import android.text.TextUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import dummydomain.yetanothercallblocker.Settings;
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabase;
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabaseItem;
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabase;
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabaseItem;
public class NumberInfoService {
public interface HiddenNumberDetector {
boolean isHiddenNumber(String number);
}
private static final Logger LOG = LoggerFactory.getLogger(NumberInfoService.class);
protected final Settings settings;
protected final HiddenNumberDetector hiddenNumberDetector;
protected final CommunityDatabase communityDatabase;
protected final FeaturedDatabase featuredDatabase;
protected final ContactsProvider contactsProvider;
protected final BlacklistService blacklistService;
public NumberInfoService(Settings settings, HiddenNumberDetector hiddenNumberDetector,
CommunityDatabase communityDatabase, FeaturedDatabase featuredDatabase,
ContactsProvider contactsProvider, BlacklistService blacklistService) {
this.settings = settings;
this.hiddenNumberDetector = hiddenNumberDetector;
this.communityDatabase = communityDatabase;
this.featuredDatabase = featuredDatabase;
this.contactsProvider = contactsProvider;
this.blacklistService = blacklistService;
}
public NumberInfo getNumberInfo(String number, boolean full) {
LOG.debug("getNumberInfo({}, {}) started", number, full);
// TODO: check number format
NumberInfo numberInfo = new NumberInfo();
numberInfo.number = number;
if (hiddenNumberDetector != null) {
numberInfo.isHiddenNumber = hiddenNumberDetector.isHiddenNumber(number);
}
LOG.trace("getNumberInfo() isHiddenNumber={}", numberInfo.isHiddenNumber);
if (numberInfo.isHiddenNumber || TextUtils.isEmpty(number)
|| TextUtils.getTrimmedLength(number) == 0) {
numberInfo.noNumber = true;
}
LOG.trace("getNumberInfo() noNumber={}", numberInfo.noNumber);
if (numberInfo.noNumber) {
numberInfo.blockingReason = getBlockingReason(numberInfo);
LOG.trace("getNumberInfo() blockingReason={}", numberInfo.blockingReason);
LOG.debug("getNumberInfo() finished early");
return numberInfo;
}
if (contactsProvider != null) {
numberInfo.contactItem = contactsProvider.get(number);
}
LOG.trace("getNumberInfo() contactItem={}", numberInfo.contactItem);
if (communityDatabase != null) {
numberInfo.communityDatabaseItem = communityDatabase.getDbItemByNumber(number);
}
LOG.trace("getNumberInfo() communityItem={}", numberInfo.communityDatabaseItem);
if (featuredDatabase != null) {
numberInfo.featuredDatabaseItem = featuredDatabase.getDbItemByNumber(number);
}
LOG.trace("getNumberInfo() featuredItem={}", numberInfo.featuredDatabaseItem);
ContactItem contactItem = numberInfo.contactItem;
FeaturedDatabaseItem featuredItem = numberInfo.featuredDatabaseItem;
if (contactItem != null && !TextUtils.isEmpty(contactItem.displayName)) {
numberInfo.name = contactItem.displayName;
} else if (featuredItem != null && !TextUtils.isEmpty(featuredItem.getName())) {
numberInfo.name = featuredItem.getName();
}
LOG.trace("getNumberInfo() name={}", numberInfo.name);
CommunityDatabaseItem communityItem = numberInfo.communityDatabaseItem;
if (communityItem != null && communityItem.hasRatings()) {
if (communityItem.getNegativeRatingsCount() > communityItem.getPositiveRatingsCount()
+ communityItem.getNeutralRatingsCount()) {
numberInfo.rating = NumberInfo.Rating.NEGATIVE;
} else if (communityItem.getPositiveRatingsCount() > communityItem.getNeutralRatingsCount()
+ communityItem.getNegativeRatingsCount()) {
numberInfo.rating = NumberInfo.Rating.POSITIVE;
} else if (communityItem.getNeutralRatingsCount() > communityItem.getPositiveRatingsCount()
+ communityItem.getNegativeRatingsCount()) {
numberInfo.rating = NumberInfo.Rating.NEUTRAL;
}
}
LOG.trace("getNumberInfo() rating={}", numberInfo.rating);
if (blacklistService != null && settings.getBlacklistIsNotEmpty()) {
// avoid loading blacklist if blocking for other reason
if (full || getBlockingReason(numberInfo) == null) {
numberInfo.blacklistItem = blacklistService.getBlacklistItemForNumber(number);
}
}
LOG.trace("getNumberInfo() blacklistItem={}", numberInfo.blacklistItem);
numberInfo.blockingReason = getBlockingReason(numberInfo);
LOG.trace("getNumberInfo() blockingReason={}", numberInfo.blockingReason);
LOG.debug("getNumberInfo() finished");
return numberInfo;
}
protected NumberInfo.BlockingReason getBlockingReason(NumberInfo numberInfo) {
if (numberInfo.contactItem != null) return null;
if (numberInfo.isHiddenNumber && settings.getBlockHiddenNumbers()) {
return NumberInfo.BlockingReason.HIDDEN_NUMBER;
}
if (numberInfo.rating == NumberInfo.Rating.NEGATIVE
&& settings.getBlockNegativeSiaNumbers()) {
return NumberInfo.BlockingReason.SIA_RATING;
}
if (numberInfo.blacklistItem != null && settings.getBlockBlacklisted()) {
return NumberInfo.BlockingReason.BLACKLISTED;
}
return null;
}
public boolean shouldBlock(NumberInfo numberInfo) {
return numberInfo.blockingReason != null;
}
public void blockedCall(NumberInfo numberInfo) {
if (blacklistService != null && numberInfo.blacklistItem != null
&& numberInfo.blockingReason == NumberInfo.BlockingReason.BLACKLISTED) {
blacklistService.addCall(numberInfo.blacklistItem, new Date());
}
}
}

View File

@ -0,0 +1,93 @@
package dummydomain.yetanothercallblocker.data.db;
import org.greenrobot.greendao.Property;
import org.greenrobot.greendao.internal.SqlUtils;
import org.greenrobot.greendao.query.CloseableListIterator;
import org.greenrobot.greendao.query.QueryBuilder;
import org.greenrobot.greendao.query.WhereCondition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
public class BlacklistDao {
public interface DaoSessionProvider {
DaoSession getDaoSession();
}
private static final Logger LOG = LoggerFactory.getLogger(BlacklistDao.class);
private final DaoSessionProvider daoSessionProvider;
public BlacklistDao(DaoSessionProvider daoSessionProvider) {
this.daoSessionProvider = daoSessionProvider;
}
public List<BlacklistItem> loadAll() {
return getBlacklistItemDao().queryBuilder()
.orderAsc(BlacklistItemDao.Properties.Pattern)
.list();
}
public <T extends Collection<BlacklistItem>> T detach(T items) {
BlacklistItemDao dao = getBlacklistItemDao();
for (BlacklistItem item : items) {
dao.detach(item);
}
return items;
}
public BlacklistItem findById(long id) {
return getBlacklistItemDao().load(id);
}
public void save(BlacklistItem blacklistItem) {
getBlacklistItemDao().save(blacklistItem);
}
public void delete(Iterable<Long> keys) {
getBlacklistItemDao().deleteByKeyInTx(keys);
}
public long countValid() {
return getBlacklistItemDao().queryBuilder()
.where(BlacklistItemDao.Properties.Invalid.notEq(true)).count();
}
public BlacklistItem getFirstMatch(String number) {
try (CloseableListIterator<BlacklistItem> it = getMatchesQueryBuilder(number).build()
.listIterator()) {
if (it.hasNext()) return it.next();
} catch (IOException e) {
LOG.debug("getFirstMatch()", e);
}
return null;
}
private QueryBuilder<BlacklistItem> getMatchesQueryBuilder(String number) {
return getBlacklistItemDao().queryBuilder()
.where(BlacklistItemDao.Properties.Invalid.notEq(true),
new InverseLikeCondition(BlacklistItemDao.Properties.Pattern, number))
.orderAsc(BlacklistItemDao.Properties.CreationDate);
}
private BlacklistItemDao getBlacklistItemDao() {
return daoSessionProvider.getDaoSession().getBlacklistItemDao();
}
private static class InverseLikeCondition extends WhereCondition.PropertyCondition {
InverseLikeCondition(Property property, String value) {
super(property, " ? LIKE ", value);
}
@Override
public void appendTo(StringBuilder builder, String tableAlias) {
builder.append(op);
SqlUtils.appendProperty(builder, tableAlias, property);
}
}
}

View File

@ -0,0 +1,131 @@
package dummydomain.yetanothercallblocker.data.db;
import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Generated;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Index;
import org.greenrobot.greendao.annotation.NotNull;
import org.greenrobot.greendao.annotation.Transient;
import java.util.Date;
import static dummydomain.yetanothercallblocker.data.BlacklistUtils.patternFromHumanReadable;
import static dummydomain.yetanothercallblocker.data.BlacklistUtils.patternToHumanReadable;
@Entity
public class BlacklistItem {
@Id
private Long id;
private String name;
@Index
@NotNull
private String pattern;
@Transient
private String humanReadablePattern;
@NotNull
private Date creationDate;
@NotNull
private boolean invalid;
@NotNull
private int numberOfCalls = 0;
private Date lastCallDate;
public BlacklistItem() {}
public BlacklistItem(String name, String pattern) {
this(null, name, patternFromHumanReadable(pattern), new Date(), false, 0, null);
}
@Generated(hash = 1295831)
public BlacklistItem(Long id, String name, @NotNull String pattern,
@NotNull Date creationDate, boolean invalid, int numberOfCalls,
Date lastCallDate) {
this.id = id;
this.name = name;
this.pattern = pattern;
this.creationDate = creationDate;
this.invalid = invalid;
this.numberOfCalls = numberOfCalls;
this.lastCallDate = lastCallDate;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPattern() {
return this.pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
this.humanReadablePattern = null;
}
public String getHumanReadablePattern() {
if (humanReadablePattern == null) {
humanReadablePattern = patternToHumanReadable(pattern);
}
return humanReadablePattern;
}
public Date getCreationDate() {
return this.creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public boolean getInvalid() {
return this.invalid;
}
public void setInvalid(boolean invalid) {
this.invalid = invalid;
}
public Date getLastCallDate() {
return this.lastCallDate;
}
public void setLastCallDate(Date lastCallDate) {
this.lastCallDate = lastCallDate;
}
public int getNumberOfCalls() {
return this.numberOfCalls;
}
public void setNumberOfCalls(int numberOfCalls) {
this.numberOfCalls = numberOfCalls;
}
@Override
public String toString() {
return "BlacklistItem{" +
"id=" + id +
", name='" + name + '\'' +
", pattern='" + pattern + '\'' +
'}';
}
}

View File

@ -0,0 +1,36 @@
package dummydomain.yetanothercallblocker.data.db;
import android.content.Context;
public class YacbDaoSessionFactory {
private final Context context;
private final String dbName;
private final Object lock = new Object();
private DaoSession daoSession;
public YacbDaoSessionFactory(Context context, String dbName) {
this.context = context;
this.dbName = dbName;
}
public DaoSession getDaoSession() {
DaoSession daoSession = this.daoSession;
if (daoSession == null) {
synchronized (lock) {
daoSession = this.daoSession;
if (daoSession == null) {
this.daoSession = daoSession = initDaoSession();
}
}
}
return daoSession;
}
private DaoSession initDaoSession() {
YacbDbOpenHelper dbOpenHelper = new YacbDbOpenHelper(context, dbName);
return new DaoMaster(dbOpenHelper.getWritableDb()).newSession();
}
}

View File

@ -0,0 +1,26 @@
package dummydomain.yetanothercallblocker.data.db;
import android.content.Context;
import org.greenrobot.greendao.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class YacbDbOpenHelper extends DaoMaster.OpenHelper {
private static final Logger LOG = LoggerFactory.getLogger(YacbDbOpenHelper.class);
public YacbDbOpenHelper(Context context, String name) {
super(context, name);
}
@Override
public void onUpgrade(Database db, int oldVersion, int newVersion) {
LOG.info("onUpgrade() oldVersion={}, newVersion={}", oldVersion, newVersion);
// upgrade
LOG.info("onUpgrade() finished");
}
}

View File

@ -0,0 +1,3 @@
package dummydomain.yetanothercallblocker.event;
public class BlacklistChangedEvent {}

View File

@ -0,0 +1,3 @@
package dummydomain.yetanothercallblocker.event;
public class BlacklistItemChangedEvent extends BlacklistChangedEvent {}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://gist.github.com/mikovali/9e57c559bc459c932bc984b64f489090 -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector>
<item android:state_activated="true">
<color android:color="?attr/colorControlActivated" />
</item>
</selector>
</item>
<item android:drawable="?attr/selectableItemBackground" />
</layer-list>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
tools:context=".BlacklistActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/blacklistItemsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/blacklist_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:contentDescription="@string/blacklist_add"
android:onClick="onAddClicked"
android:src="@drawable/ic_plus_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".EditBlacklistItemActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/text_margin">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameTextField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/edit_blacklist_item_name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences|textAutoCorrect"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/patternTextField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/edit_blacklist_item_number_pattern">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/patternEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/number_pattern_hint" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,51 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator"
android:gravity="center_vertical|start"
android:orientation="horizontal"
android:paddingLeft="@dimen/item_padding"
android:paddingTop="8dp"
android:paddingRight="@dimen/item_padding"
android:paddingBottom="8dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Annoying spammer" />
<TextView
android:id="@+id/pattern"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:text="+123456789##" />
<TextView
android:id="@+id/stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="3 calls, last: Aug 01, 2020 10:10" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/number_pattern_incorrect"
android:src="@drawable/ic_error_24dp"
app:tint="@color/design_default_color_error" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_block_blacklisted"
android:checkable="true"
android:onClick="onBlockBlacklistedChanged"
android:title="@string/block_blacklisted_short" />
</menu>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_delete"
android:icon="@drawable/ic_delete_24dp"
android:title="@string/blacklist_delete"
app:showAsAction="ifRoom" />
</menu>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_check_24dp"
android:onClick="onSaveClicked"
android:title="@string/save"
app:showAsAction="always" />
<item
android:id="@+id/menu_delete"
android:onClick="onDeleteClicked"
android:title="@string/blacklist_delete" />
</menu>

View File

@ -25,6 +25,10 @@
android:onClick="onUseContactsChanged"
android:title="@string/use_contacts" />
<item
android:onClick="onOpenBlacklist"
android:title="@string/open_blacklist_activity" />
<item
android:onClick="onOpenSettings"
android:title="@string/open_settings_activity" />

View File

@ -70,7 +70,7 @@
<string name="back">Atrás</string>
<string name="no">No</string>
<string name="load_reviews_confirmation_title">¿Estás seguro?</string>
<string name="are_you_sure">¿Estás seguro?</string>
<string name="load_reviews_confirmation_message">Cargar las revisiones online filtrará el número a un servicio de terceros. ¿Estás seguro de que quieres hacerlo con un número presente en tus contactos?</string>
<string name="open_settings_activity">Ajustes</string>

View File

@ -74,7 +74,7 @@
<string name="back">Retour</string>
<string name="no">Non</string>
<string name="load_reviews_confirmation_title">Êtes-vous sûr ?</string>
<string name="are_you_sure">Êtes-vous sûr ?</string>
<string name="load_reviews_confirmation_message">La transmission d\'évaluations en ligne divulguera le numéro à un service tiers. Êtes-vous sûr de vouloir le faire avec un numéro présent dans vos contacts ?</string>
<string name="open_settings_activity">Paramètres</string>

View File

@ -70,7 +70,7 @@
<string name="back">Terug</string>
<string name="no">Nee</string>
<string name="load_reviews_confirmation_title">Weet je het zeker?</string>
<string name="are_you_sure">Weet je het zeker?</string>
<string name="load_reviews_confirmation_message">Door online-recensies te laden, lekt het nummer uit naar een externe dienst. Weet je zeker dat je dit wilt doen met een nummer uit je contactpersonenlijst?</string>
<string name="incoming_call_notifications">Inkomende oproep-meldingen</string>

View File

@ -25,6 +25,9 @@
<string name="block_negative_sia_numbers_summary">Блокировать звонки c номеров с отрицательным рейтингом</string>
<string name="block_hidden_number">Блокировать скрытые номера</string>
<string name="block_hidden_number_summary">Блокировать звонки со скрытых номеров. Экспериментальная функция. Вероятно, работает лучше в "продвинутом режиме блокирования". Пожалуйста, сообщите о своём опыте использования в репозиторий на gitlab</string>
<string name="block_blacklisted_short">Блокировать из ЧС</string>
<string name="block_blacklisted">Блокировать из чёрного списка</string>
<string name="block_blacklisted_summary">Блокировать звонки с номеров добавленных в чёрный список</string>
<string name="use_call_screening_service">Продвинутый режим блокирования вызовов</string>
<string name="use_call_screening_service_summary">Позволяет блокировать вызовы до того, как телефон зазвонит. Это требует назначить приложение \"приложением для звонков\" (на Android 79) или \"приложением для АОН и защиты от спама\" (на Android 10+)</string>
<string name="use_call_screening_service_disable_message">Выберите другое приложение для звонков или защиты от спама в меню Настройки - Приложения - Приложения по умолчанию</string>
@ -73,8 +76,9 @@
<string name="online_reviews">Online-отзывы</string>
<string name="add_web_review">Добавить отзыв (веб)</string>
<string name="back">Назад</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="load_reviews_confirmation_title">Вы уверены?</string>
<string name="are_you_sure">Вы уверены?</string>
<string name="load_reviews_confirmation_message">Это номер из адресной книги! Для получения отзывов номер передается в сторонний сервис и может попасть к третьим лицам. Вы точно хотите посмотреть отзывы на этот номер?</string>
<string name="call_log_permission_message">Разрешите доступ к журналу вызовов, чтобы здесь отображались недавние вызовы</string>
<string name="notification_background_operation">Выполняется процесс в фоне…</string>
@ -114,4 +118,25 @@
<string name="export_logcat">Экспортировать logcat</string>
<string name="export_logcat_summary">Экспортировать и поделиться содержимым logcat с приложением по вашему выбору (например, с email-клиентом). Отчёты могут содержать конфиденциальные данные (номера телефонов, имена контактов). Только выбранное приложение получит доступ к этим данным</string>
<string name="no_number"><![CDATA[<нет номера>]]></string>
<string name="save">Сохранить</string>
<string name="selected_count">Выбрано: %1$d</string>
<string name="open_blacklist_activity">Чёрный список</string>
<string name="title_blacklist_activity">Чёрный список</string>
<plurals name="blacklist_item_stats">
<item quantity="one">Звонил %2$s</item>
<item quantity="few">%1$d звонка, последний: %2$s</item>
<item quantity="many">%1$d звонков, последний: %2$s</item>
</plurals>
<string name="blacklist_item_date_no_info">нет информации</string>
<string name="blacklist_add">Добавить</string>
<string name="blacklist_delete">Удалить</string>
<string name="blacklist_delete_confirmation">Удалить выбранные элементы?</string>
<string name="title_add_blacklist_item_activity">Добавить</string>
<string name="title_edit_blacklist_item_activity">Редактировать</string>
<string name="edit_blacklist_item_name">Имя</string>
<string name="edit_blacklist_item_number_pattern">Шаблон номера</string>
<string name="number_pattern_incorrect">Некорректный шаблон</string>
<string name="number_pattern_empty">Пустой шаблон</string>
<string name="number_pattern_hint">Введите номер в формате +СТРАНА-НОМЕР (как Android показал бы в списке вызовов). Используйте \"*\" как подстановку для нуля и более цифр и \"#\" для одной цифры.</string>
</resources>

View File

@ -2,4 +2,5 @@
<dimen name="app_bar_height">180dp</dimen>
<dimen name="text_margin">16dp</dimen>
<dimen name="item_padding">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -72,9 +72,10 @@
<string name="online_reviews">Online reviews</string>
<string name="add_web_review">Add review (web)</string>
<string name="back">Back</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="load_reviews_confirmation_title">Are you sure?</string>
<string name="are_you_sure">Are you sure?</string>
<string name="load_reviews_confirmation_message">Loading online reviews will leak the number to a 3rd party service. Are you sure you want to do it with a number present in your Contacts?</string>
<string name="open_settings_activity">Settings</string>
@ -94,6 +95,9 @@
<string name="block_negative_sia_numbers_summary">Block calls from numbers with negative rating (based on a community database)</string>
<string name="block_hidden_number">Block hidden numbers</string>
<string name="block_hidden_number_summary">Block calls from hidden numbers. Experimental. Probably works better in \"advanced call blocking mode\". Please report your experience in the repo issues on gitlab</string>
<string name="block_blacklisted_short">Block blacklisted</string>
<string name="block_blacklisted">Block blacklisted numbers</string>
<string name="block_blacklisted_summary">Block calls from numbers added to the blacklist</string>
<string name="use_call_screening_service">Advanced call blocking mode</string>
<string name="use_call_screening_service_summary">Allows to block calls before the phone starts to ring. Requires the app to be set as the \"Phone app\" (Android 79) or as the \"Caller ID app\" (Android 10+)</string>
<string name="use_call_screening_service_disable_message">Select different \"Phone app\" or \"Caller ID app\" in Settings - Apps - Default apps</string>
@ -126,6 +130,26 @@
<string name="export_logcat">Export logcat</string>
<string name="export_logcat_summary">Export and share logcat contents with an app of your choosing (for example with an email client). The reports may contain sensitive data (phone numbers, contact names). Only the chosen app will have access to this data</string>
<string name="no_number"><![CDATA[<no number>]]></string>
<string name="save">Save</string>
<string name="selected_count">%1$d selected</string>
<string name="open_blacklist_activity">Blacklist</string>
<string name="title_blacklist_activity">Blacklist</string>
<plurals name="blacklist_item_stats">
<item quantity="one">Called at %2$s</item>
<item quantity="other">%1$d calls, last: %2$s</item>
</plurals>
<string name="blacklist_item_date_no_info">no info</string>
<string name="blacklist_add">Add</string>
<string name="blacklist_delete">Delete</string>
<string name="blacklist_delete_confirmation">Delete the selected items?</string>
<string name="title_add_blacklist_item_activity">Add number</string>
<string name="title_edit_blacklist_item_activity">Edit number</string>
<string name="edit_blacklist_item_name">Name</string>
<string name="edit_blacklist_item_number_pattern">Number pattern</string>
<string name="number_pattern_incorrect">Incorrect pattern</string>
<string name="number_pattern_empty">Empty pattern</string>
<string name="number_pattern_hint">Enter the number in +COUNTRY-NUMBER format (as Android would show in your dialer). Use \"*\" as a wildcard for zero or more digits, and \"#\" for exactly one digit.</string>
<string name="open_debug_activity">Open debug screen</string>
<string name="debug_activity_label">Debug</string>

View File

@ -7,6 +7,7 @@
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="android:activatedBackgroundIndicator">@drawable/activated_background</item>
</style>
<style name="AppTheme.NoActionBar">

View File

@ -38,6 +38,11 @@
app:key="blockHiddenNumbers"
app:summary="@string/block_hidden_number_summary"
app:title="@string/block_hidden_number" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:key="blockBlacklisted"
app:summary="@string/block_blacklisted_summary"
app:title="@string/block_blacklisted" />
<SwitchPreferenceCompat
app:key="useCallScreeningService"
app:persistent="false"

View File

@ -5,6 +5,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
}
}