Blacklist import/export

This commit is contained in:
xynngh 2020-08-07 13:23:05 +04:00
parent 42244725f4
commit d44121607f
11 changed files with 485 additions and 27 deletions

View File

@ -45,6 +45,7 @@ dependencies {
//noinspection GradleDependency: 3.12.* is the latest version compatible with Android <5
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
implementation 'com.gitlab.xynngh:LibPhoneNumberInfo:fd60ad9583'
implementation 'org.apache.commons:commons-csv:1.8'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

View File

@ -1,15 +1,19 @@
package dummydomain.yetanothercallblocker;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
@ -19,17 +23,29 @@ import androidx.recyclerview.widget.RecyclerView;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import dummydomain.yetanothercallblocker.data.BlacklistImporterExporter;
import dummydomain.yetanothercallblocker.data.BlacklistService;
import dummydomain.yetanothercallblocker.data.YacbHolder;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
import dummydomain.yetanothercallblocker.event.BlacklistChangedEvent;
import dummydomain.yetanothercallblocker.utils.FileUtils;
public class BlacklistActivity extends AppCompatActivity {
private static final int REQUEST_CODE_IMPORT = 1;
private static final Logger LOG = LoggerFactory.getLogger(BlacklistActivity.class);
private final Settings settings = App.getSettings();
private final BlacklistDao blacklistDao = YacbHolder.getBlacklistDao();
private final BlacklistService blacklistService = YacbHolder.getBlacklistService();
@ -172,6 +188,38 @@ public class BlacklistActivity extends AppCompatActivity {
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_IMPORT && resultCode == Activity.RESULT_OK
&& data != null && data.getData() != null) {
boolean error = false;
ParcelFileDescriptor pfd = null;
try {
pfd = getContentResolver().openFileDescriptor(data.getData(), "r");
} catch (FileNotFoundException e) {
error = true;
LOG.warn("onActivityResult() get file for import result", e);
}
if (pfd != null) {
if (new BlacklistImporterExporter().importBlacklist(
YacbHolder.getBlacklistDao(), YacbHolder.getBlacklistService(),
pfd.getFileDescriptor())) {
Toast.makeText(this, R.string.done, Toast.LENGTH_SHORT).show();
} else {
error = true;
}
}
if (error) {
Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void onBlacklistChanged(BlacklistChangedEvent blacklistChangedEvent) {
loadItems();
@ -214,4 +262,42 @@ public class BlacklistActivity extends AppCompatActivity {
startActivity(EditBlacklistItemActivity.getIntent(this, blacklistItem.getId()));
}
public void onExportBlacklistClicked(MenuItem item) {
File file = exportBlacklist();
if (file != null) {
FileUtils.shareFile(this, file);
} else {
Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
}
}
private File exportBlacklist() {
File file = new File(getCacheDir(), "YetAnotherCallBlocker_backup.csv");
try {
if (!file.exists() && !file.createNewFile()) return null;
try (FileWriter writer = new FileWriter(file)) {
if (new BlacklistImporterExporter().writeBackup(blacklistDao.loadAll(), writer)) {
return file;
}
}
} catch (IOException e) {
LOG.warn("exportBlacklist()", e);
}
return null;
}
public void onImportBlacklistClicked(MenuItem item) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_CODE_IMPORT);
} else {
Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -0,0 +1,322 @@
package dummydomain.yetanothercallblocker.data;
import android.text.TextUtils;
import androidx.core.util.ObjectsCompat;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
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.isValidPattern;
import static dummydomain.yetanothercallblocker.data.BlacklistUtils.patternFromHumanReadable;
import static dummydomain.yetanothercallblocker.data.BlacklistUtils.patternToHumanReadable;
public class BlacklistImporterExporter {
private static final Logger LOG = LoggerFactory.getLogger(BlacklistImporterExporter.class);
private static final String HEADER_ID = "ID";
private static final String HEADER_NAME = "name";
private static final String HEADER_PATTERN = "pattern";
public boolean writeBackup(Iterable<BlacklistItem> blacklistItems, Appendable out) {
try (CSVPrinter printer = CSVFormat.DEFAULT.print(out)) {
printer.printRecord(HEADER_ID, HEADER_NAME, HEADER_PATTERN,
"creationTimestamp", "numberOfCalls", "lastCallTimestamp");
for (BlacklistItem item : blacklistItems) {
printer.printRecord(item.getId(), item.getName(),
patternToHumanReadable(item.getPattern()),
item.getCreationDate().getTime(), item.getNumberOfCalls(),
item.getLastCallDate() != null ? item.getLastCallDate().getTime() : "");
}
} catch (IOException e) {
LOG.warn("write()", e);
return false;
}
return true;
}
public boolean importBlacklist(BlacklistDao blacklistDao, BlacklistService blacklistService,
FileDescriptor fileDescriptor) {
List<BlacklistItem> items = null;
try (FileInputStream inputStream = new FileInputStream(fileDescriptor)) {
items = read(inputStream);
} catch (IOException e) {
LOG.warn("importBlacklist()", e);
}
if (items == null) return false;
for (BlacklistItem item : items) {
BlacklistItem existingItem = null;
if (item.getId() != null) {
existingItem = blacklistDao.findById(item.getId());
if (existingItem != null && !ObjectsCompat
.equals(item.getPattern(), existingItem.getPattern())) {
item.setId(null);
existingItem = null;
}
}
if (existingItem == null) {
existingItem = blacklistDao.findByPattern(item.getPattern());
}
if (existingItem != null) {
boolean changed = false;
if (TextUtils.isEmpty(existingItem.getName())
&& !TextUtils.isEmpty(item.getName())) {
existingItem.setName(item.getName());
changed = true;
}
if (existingItem.getNumberOfCalls() < item.getNumberOfCalls()) {
existingItem.setNumberOfCalls(item.getNumberOfCalls());
changed = true;
}
if (item.getLastCallDate() != null && (existingItem.getLastCallDate() == null
|| existingItem.getLastCallDate().before(item.getLastCallDate()))) {
existingItem.setLastCallDate(item.getLastCallDate());
changed = true;
}
if (changed) {
blacklistService.save(existingItem);
}
} else {
blacklistService.insert(item);
}
}
return true;
}
public List<BlacklistItem> read(InputStream inputStream) throws IOException {
List<BlacklistItem> items = null;
try (BufferedInputStream bis = new BufferedInputStream(inputStream)) {
bis.mark(4000);
if (isYacbBackup(new InputStreamReader(bis))) {
bis.reset();
LOG.info("importBlacklist() importing as YACB backup");
items = readYacbBackup(new InputStreamReader(bis));
} else {
LOG.debug("importBlacklist() not a YACB backup");
bis.reset();
if (isNoPhoneSpamBackup(new InputStreamReader(bis))) {
bis.reset();
LOG.info("importBlacklist() trying to import as NoPhoneSpam backup");
items = readNoPhoneSpamBackup(new InputStreamReader(bis));
} else {
LOG.debug("importBlacklist() not a NoPhoneSpam backup");
}
}
}
return items;
}
public boolean isYacbBackup(Reader in) {
try {
CSVParser parser = CSVFormat.DEFAULT.parse(in); // do NOT close
Iterator<CSVRecord> iterator = parser.iterator();
if (iterator.hasNext()) {
CSVRecord record = iterator.next();
if (record.size() < 6) return false;
boolean foundHeader = checkYacbHeader(record);
LOG.debug("isYacbBackup() found header={}", foundHeader);
if (foundHeader) return true;
// check that the types match
try {
Long.parseLong(record.get(0));
String creationTimestampString = record.get(3);
if (!TextUtils.isEmpty(creationTimestampString)) {
Long.parseLong(creationTimestampString);
}
Integer.parseInt(record.get(4));
String lastCallTimestampString = record.get(5);
if (!TextUtils.isEmpty(lastCallTimestampString)) {
Long.parseLong(lastCallTimestampString);
}
} catch (Exception e) {
LOG.debug("isYacbBackup() error parsing item", e);
return false;
}
return true;
}
} catch (IOException e) {
LOG.debug("isYacbBackup()", e);
return false;
}
LOG.debug("isYacbBackup() empty file?");
return true;
}
private boolean checkYacbHeader(CSVRecord record) {
boolean foundHeader = false;
try {
foundHeader = HEADER_ID.equals(record.get(0))
&& HEADER_NAME.equals(record.get(1))
&& HEADER_PATTERN.equals(record.get(2));
} catch (Exception e) {
LOG.warn("checkYacbHeader() error checking header", e);
}
return foundHeader;
}
public List<BlacklistItem> readYacbBackup(Reader in) {
try (CSVParser parser = CSVFormat.DEFAULT.parse(in)) {
List<BlacklistItem> blacklistItems = new ArrayList<>();
boolean first = true;
for (CSVRecord record : parser) {
if (first) {
first = false;
boolean foundHeader = checkYacbHeader(record);
LOG.debug("readYacbBackup() found header={}", foundHeader);
if (foundHeader) {
continue;
}
}
BlacklistItem item = new BlacklistItem();
boolean enough = false;
try {
int i = 0;
item.setId(Long.valueOf(record.get(i++)));
item.setName(record.get(i++));
item.setPattern(cleanPattern(patternFromHumanReadable(record.get(i++))));
enough = true;
String creationTimestampString = record.get(i++);
if (!TextUtils.isEmpty(creationTimestampString)) {
item.setCreationDate(new Date(Long.parseLong(creationTimestampString)));
}
item.setNumberOfCalls(Integer.parseInt(record.get(i++)));
String lastCallTimestampString = record.get(i);
if (!TextUtils.isEmpty(lastCallTimestampString)) {
item.setLastCallDate(new Date(Long.parseLong(lastCallTimestampString)));
}
} catch (Exception e) {
LOG.warn("readYacbBackup() error parsing item", e);
}
LOG.trace("readYacbBackup() enough={}", enough);
if (enough) {
blacklistItems.add(sanitize(item));
}
}
return blacklistItems;
} catch (IOException e) {
LOG.warn("readYacbBackup()", e);
return null;
}
}
public boolean isNoPhoneSpamBackup(Reader in) {
try {
BufferedReader br = new BufferedReader(in); // do NOT close
String delimiter = ": ";
String line = br.readLine();
if (line != null) {
return line.contains(delimiter);
}
} catch (IOException e) {
LOG.warn("isNoPhoneSpamBackup()", e);
return false;
}
return true;
}
public List<BlacklistItem> readNoPhoneSpamBackup(Reader in) {
try (BufferedReader br = new BufferedReader(in)) {
List<BlacklistItem> blacklistItems = new ArrayList<>();
String delimiter = ": ";
String line;
while ((line = br.readLine()) != null) {
int delimiterIndex = line.indexOf(delimiter);
if (delimiterIndex == -1) {
LOG.warn("readNoPhoneSpamBackup() incorrect format: no delimiter");
continue;
}
String pattern = line.substring(0, delimiterIndex).trim();
String name = line.substring(delimiterIndex + delimiter.length());
BlacklistItem item = new BlacklistItem(name,
cleanPattern(patternFromHumanReadable(pattern)));
blacklistItems.add(sanitize(item));
}
return blacklistItems;
} catch (IOException e) {
LOG.warn("readNoPhoneSpamBackup()", e);
return null;
}
}
private BlacklistItem sanitize(BlacklistItem item) {
if (item.getId() != null && item.getId() < 1) {
item.setId(null);
}
item.setInvalid(!isValidPattern(item.getPattern()));
if (item.getCreationDate() == null) {
item.setCreationDate(new Date());
}
if (item.getNumberOfCalls() < 0) {
item.setNumberOfCalls(0);
}
return item;
}
}

View File

@ -37,17 +37,25 @@ public class BlacklistService {
public void save(BlacklistItem blacklistItem) {
boolean newItem = blacklistItem.getId() == null;
blacklistItem.setInvalid(!BlacklistUtils.isValidPattern(blacklistItem.getPattern()));
sanitize(blacklistItem);
blacklistDao.save(blacklistItem);
blacklistChanged();
blacklistChanged(!newItem);
}
postEvent(newItem ? new BlacklistChangedEvent() : new BlacklistItemChangedEvent());
public void insert(BlacklistItem blacklistItem) {
sanitize(blacklistItem);
blacklistDao.insert(blacklistItem);
blacklistChanged(false);
}
public void addCall(BlacklistItem blacklistItem, Date date) {
sanitize(blacklistItem);
blacklistItem.setLastCallDate(Objects.requireNonNull(date));
blacklistItem.setNumberOfCalls(blacklistItem.getNumberOfCalls() + 1);
blacklistDao.save(blacklistItem);
postEvent(new BlacklistItemChangedEvent());
@ -56,11 +64,19 @@ public class BlacklistService {
public void delete(Iterable<Long> keys) {
blacklistDao.delete(keys);
blacklistChanged();
blacklistChanged(false);
}
private void blacklistChanged() {
private void sanitize(BlacklistItem blacklistItem) {
blacklistItem.setInvalid(!BlacklistUtils.isValidPattern(blacklistItem.getPattern()));
if (blacklistItem.getCreationDate() == null) blacklistItem.setCreationDate(new Date());
if (blacklistItem.getNumberOfCalls() < 0) blacklistItem.setNumberOfCalls(0);
}
private void blacklistChanged(boolean itemUpdate) {
callback.changed(blacklistDao.countValid() != 0);
postEvent(itemUpdate ? new BlacklistItemChangedEvent() : new BlacklistChangedEvent());
}
}

View File

@ -3,6 +3,7 @@ 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.Query;
import org.greenrobot.greendao.query.QueryBuilder;
import org.greenrobot.greendao.query.WhereCondition;
import org.slf4j.Logger;
@ -44,10 +45,20 @@ public class BlacklistDao {
return getBlacklistItemDao().load(id);
}
public BlacklistItem findByPattern(String pattern) {
return first(getBlacklistItemDao().queryBuilder()
.where(BlacklistItemDao.Properties.Pattern.eq(pattern))
.orderAsc(BlacklistItemDao.Properties.Pattern));
}
public void save(BlacklistItem blacklistItem) {
getBlacklistItemDao().save(blacklistItem);
}
public void insert(BlacklistItem blacklistItem) {
getBlacklistItemDao().insert(blacklistItem);
}
public void delete(Iterable<Long> keys) {
getBlacklistItemDao().deleteByKeyInTx(keys);
}
@ -58,13 +69,7 @@ public class BlacklistDao {
}
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;
return first(getMatchesQueryBuilder(number));
}
private QueryBuilder<BlacklistItem> getMatchesQueryBuilder(String number) {
@ -74,6 +79,19 @@ public class BlacklistDao {
.orderAsc(BlacklistItemDao.Properties.CreationDate);
}
private <T> T first(QueryBuilder<T> queryBuilder) {
return first(queryBuilder.build());
}
private <T> T first(Query<T> query) {
try (CloseableListIterator<T> it = query.listIterator()) {
if (it.hasNext()) return it.next();
} catch (IOException e) {
LOG.debug("first()", e);
}
return null;
}
private BlacklistItemDao getBlacklistItemDao() {
return daoSessionProvider.getDaoSession().getBlacklistItemDao();
}

View File

@ -4,8 +4,6 @@ import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.core.content.ContextCompat;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
@ -125,23 +123,12 @@ public class DebuggingUtils {
private static File getFilesDir(Context context, boolean external) {
if (external) {
File[] dirs = ContextCompat.getExternalFilesDirs(context, null);
for (File dir : dirs) {
if (dir != null) return dir;
}
File f = FileUtils.getExternalFilesDir(context);
if (f != null) return f;
Log.d(TAG, "getFilesDir() no external dirs available");
}
/*
not secure enough
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!context.isDeviceProtectedStorage()) {
context = context.createDeviceProtectedStorageContext();
}
}
*/
return context.getCacheDir();
}

View File

@ -1,10 +1,12 @@
package dummydomain.yetanothercallblocker.utils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.core.app.ShareCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import org.slf4j.Logger;
@ -34,4 +36,12 @@ public class FileUtils {
}
}
public static File getExternalFilesDir(Context context) {
File[] dirs = ContextCompat.getExternalFilesDirs(context, null);
for (File dir : dirs) {
if (dir != null) return dir;
}
return null;
}
}

View File

@ -7,4 +7,12 @@
android:onClick="onBlockBlacklistedChanged"
android:title="@string/block_blacklisted_short" />
<item
android:onClick="onExportBlacklistClicked"
android:title="@string/blacklist_export" />
<item
android:onClick="onImportBlacklistClicked"
android:title="@string/blacklist_import" />
</menu>

View File

@ -123,6 +123,8 @@
<string name="save_crashes_to_external_storage_summary">Sauvegarder les rapports et les journaux d\'incidents sur le stockage public, sinon les rapports d\'incidents sont sauvegardés dans un dossier privé de l\'application. Les rapports peuvent contenir des données sensibles (numéros de téléphone, noms de contact). D\'autres applications ayant une autorisation de stockage peuvent avoir accès à ces données dans le stockage public</string>
<string name="save_logcat_on_crash">Sauvegarder les journaux de log en cas de crash</string>
<string name="save_logcat_on_crash_summary">Sauvegarder la copie du journal de log en cas de crash (en plus d\'un stacktrace de base)</string>
<string name="done">Terminé</string>
<string name="error">Erreur</string>
<string name="no_number"><![CDATA[<aucun numéro>]]></string>
<string name="open_debug_activity">Ouvrir l\'écran de débogage</string>

View File

@ -78,6 +78,8 @@
<string name="back">Назад</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="done">Выполнено</string>
<string name="error">Ошибка</string>
<string name="are_you_sure">Вы уверены?</string>
<string name="load_reviews_confirmation_message">Это номер из адресной книги! Для получения отзывов номер передается в сторонний сервис и может попасть к третьим лицам. Вы точно хотите посмотреть отзывы на этот номер?</string>
<string name="call_log_permission_message">Разрешите доступ к журналу вызовов, чтобы здесь отображались недавние вызовы</string>
@ -133,6 +135,8 @@
<string name="blacklist_add">Добавить</string>
<string name="blacklist_delete">Удалить</string>
<string name="blacklist_delete_confirmation">Удалить выбранные элементы?</string>
<string name="blacklist_export">Экспортировать</string>
<string name="blacklist_import">Импортировать</string>
<string name="title_add_blacklist_item_activity">Добавить</string>
<string name="title_edit_blacklist_item_activity">Редактировать</string>
<string name="edit_blacklist_item_name">Имя</string>

View File

@ -74,6 +74,8 @@
<string name="back">Back</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="done">Done</string>
<string name="error">Error</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>
@ -144,6 +146,8 @@
<string name="blacklist_add">Add</string>
<string name="blacklist_delete">Delete</string>
<string name="blacklist_delete_confirmation">Delete the selected items?</string>
<string name="blacklist_export">Export</string>
<string name="blacklist_import">Import</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>