Move DB-related code to a library

This commit is contained in:
xynngh 2020-06-19 16:04:09 +04:00
parent be72580311
commit 55b8c57264
25 changed files with 2 additions and 2097 deletions

View File

@ -39,7 +39,7 @@ dependencies {
implementation 'org.conscrypt:conscrypt-android:2.4.0'
//noinspection GradleDependency: 3.12.* is the latest version compatible with Android <5
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
implementation 'commons-codec:commons-codec:1.14' // beware: a version included in Android is used instead
implementation 'com.gitlab.xynngh:LibPhoneNumberInfo:096f78cc30'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

View File

@ -1,9 +0,0 @@
package dummydomain.yetanothercallblocker.sia;
public interface Properties {
int getInt(String key, int defValue);
void setInt(String key, int value);
}

View File

@ -1,13 +0,0 @@
package dummydomain.yetanothercallblocker.sia;
public interface Settings {
int getBaseDbVersion();
void setBaseDbVersion(int version);
int getSecondaryDbVersion();
void setSecondaryDbVersion(int version);
}

View File

@ -1,34 +0,0 @@
package dummydomain.yetanothercallblocker.sia;
public class SettingsImpl implements Settings {
private static final String PREF_BASE_DB_VERSION = "baseDbVersion";
private static final String PREF_SECONDARY_DB_VERSION = "secondaryDbVersion";
private final Properties props;
public SettingsImpl(Properties props) {
this.props = props;
}
@Override
public int getBaseDbVersion() {
return props.getInt(PREF_BASE_DB_VERSION, 0);
}
@Override
public void setBaseDbVersion(int version) {
props.setInt(PREF_BASE_DB_VERSION, version);
}
@Override
public int getSecondaryDbVersion() {
return props.getInt(PREF_SECONDARY_DB_VERSION, 0);
}
@Override
public void setSecondaryDbVersion(int version) {
props.setInt(PREF_SECONDARY_DB_VERSION, version);
}
}

View File

@ -1,14 +0,0 @@
package dummydomain.yetanothercallblocker.sia;
import java.io.IOException;
import java.io.InputStream;
public interface Storage {
String getDataDirPath();
String getCacheDirPath();
InputStream openFile(String fileName, boolean internal) throws IOException;
}

View File

@ -1,99 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model;
public class CommunityReview {
public enum Rating {
UNKNOWN(0),
POSITIVE(1),
NEGATIVE(2),
NEUTRAL(3);
private int id;
Rating(int id) {
this.id = id;
}
public int getId() {
return id;
}
public static Rating getById(int id) {
for (Rating rating : Rating.values()) {
if (rating.getId() == id) return rating;
}
return null;
}
}
private int id;
private Rating rating;
private NumberCategory category;
private String author;
private String title;
private String comment;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Rating getRating() {
return rating;
}
public void setRating(Rating rating) {
this.rating = rating;
}
public NumberCategory getCategory() {
return category;
}
public void setCategory(NumberCategory category) {
this.category = category;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
@Override
public String toString() {
return "CommunityReview{" +
"id=" + id +
", rating=" + rating +
", category=" + category +
", author='" + author + '\'' +
", title='" + title + '\'' +
", comment='" + comment + '\'' +
'}';
}
}

View File

@ -1,79 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dummydomain.yetanothercallblocker.sia.network.WebService;
public class CommunityReviewsLoader {
private static final Logger LOG = LoggerFactory.getLogger(CommunityReviewsLoader.class);
private final WebService webService;
private final String country;
public CommunityReviewsLoader(WebService webService, String country) {
this.webService = webService;
this.country = country;
}
public List<CommunityReview> loadReviews(String number) {
LOG.debug("loadReviews({}) started", number);
if (number.startsWith("+")) {
number = number.substring(1);
}
Map<String, String> params = new HashMap<>();
params.put("number", number);
params.put("country", country);
WebService.WSResponse response = webService.callForJson(webService.getGetReviewsUrlPart(), params);
if (response == null || !response.getSuccessful()) {
LOG.warn("loadReviews() response is not successful");
return null;
}
try {
List<CommunityReview> reviews = new ArrayList<>();
// response.getJsonObject():
// "success": boolean
// "nn": String number starting with "+"
// "count": int number of items
// "items": the following array
JSONArray items = response.getJsonObject().getJSONArray("items");
for (int i = 0; i < items.length(); i++) {
JSONObject item = items.getJSONObject(i);
CommunityReview review = new CommunityReview();
review.setId(item.getInt("id"));
review.setRating(CommunityReview.Rating.getById(item.getInt("rating")));
review.setCategory(NumberCategory.getById(item.getInt("category_id")));
review.setAuthor(item.getString("nick"));
review.setTitle(item.getString("title"));
review.setComment(item.getString("comment"));
reviews.add(review);
}
LOG.trace("loadReviews() loaded {} reviews", reviews.size());
return reviews;
} catch (JSONException e) {
LOG.error("loadReviews()", e);
}
return null;
}
}

View File

@ -1,46 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model;
public enum NumberCategory {
NONE(0),
TELEMARKETER(1),
DEPT_COLLECTOR(2),
SILENT_CALL(3),
NUISANCE_CALL(4),
UNSOLICITED_CALL(5),
CALL_CENTER(6),
FAX_MACHINE(7),
NON_PROFIT(8),
POLITICAL(9),
SCAM(10),
PRANK(11),
SMS(12),
SURVEY(13),
OTHER(14),
FINANCE_SERVICE(15),
COMPANY(16),
SERVICE(17),
ROBOCALL(18),
// TODO: check: these are probably not present in the db
SAFE_PERSONAL(100),
SAFE_COMPANY(101),
SAFE_NONPROFIT(102);
private int id;
NumberCategory(int id) {
this.id = id;
}
public int getId() {
return id;
}
public static NumberCategory getById(int id) {
for (NumberCategory category : values()) {
if (category.getId() == id) return category;
}
return null;
}
}

View File

@ -1,248 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import android.util.SparseArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import dummydomain.yetanothercallblocker.sia.Storage;
public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V> {
public enum Source {
INTERNAL, EXTERNAL, ANY
}
private static final Logger LOG = LoggerFactory.getLogger(AbstractDatabase.class);
protected final Storage storage;
protected final Source source;
protected final String pathPrefix;
protected boolean useInternal;
protected int baseDatabaseVersion;
protected int numberOfItems;
protected DatabaseDataSliceNode dbRootNode;
protected SparseArray<T> sliceCache = new SparseArray<>(); // TODO: cache cleaning
protected boolean loaded;
protected boolean failedToLoad;
public AbstractDatabase(Storage storage, Source source, String pathPrefix) {
this.storage = storage;
this.source = source;
this.pathPrefix = pathPrefix;
}
public boolean isOperational() {
checkLoaded();
return loaded;
}
public int getBaseDbVersion() {
checkLoaded();
return baseDatabaseVersion;
}
public boolean reload() {
return load();
}
protected void checkLoaded() {
if (loaded || failedToLoad) return;
LOG.debug("checkLoaded() loading DB");
load();
}
protected void reset() {
loaded = false;
failedToLoad = false;
baseDatabaseVersion = 0;
numberOfItems = 0;
dbRootNode = null;
sliceCache.clear();
}
protected boolean load() {
LOG.debug("load() loading DB");
reset();
if ((source == Source.ANY || source == Source.INTERNAL) && load(true)) {
useInternal = true;
loaded = true;
return true;
} else if ((source == Source.ANY || source == Source.EXTERNAL) && load(false)) {
useInternal = false;
loaded = true;
return true;
} else {
LOG.warn("load() failed to load DB");
failedToLoad = true;
return false;
}
}
protected boolean load(boolean useInternal) {
if (loadInfoData(useInternal) && loadSliceListData(useInternal)) {
LOG.info("load() loaded DB useInternal={}, baseDatabaseVersion={}, numberOfItems={}",
useInternal, this.baseDatabaseVersion, this.numberOfItems);
return true;
}
return false;
}
protected String getPathPrefix() {
return pathPrefix;
}
protected abstract String getNamePrefix();
protected boolean loadInfoData(boolean useInternal) {
LOG.debug("loadInfoData() started; useInternal: {}", useInternal);
String fileName = getPathPrefix() + getNamePrefix() + "info.dat";
try (InputStream is = storage.openFile(fileName, useInternal);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
String headerString = bufferedReader.readLine();
if (!"YACBSIAI".equals(headerString) && !"MDI".equals(headerString)) {
LOG.debug("loadInfoData() incorrect header: {}", headerString);
return false;
}
// NB: SIA ignores Integer.parseInt() exceptions and falls back to 0
String dbVersionString = bufferedReader.readLine();
int dbVersion = Integer.parseInt(dbVersionString);
String numberOfItemsString = bufferedReader.readLine();
int numberOfItems = Integer.parseInt(numberOfItemsString);
this.baseDatabaseVersion = dbVersion;
this.numberOfItems = numberOfItems;
LOG.debug("loadInfoData() loaded MDI, baseDatabaseVersion={}, numberOfItems={}",
this.baseDatabaseVersion, this.numberOfItems);
loadInfoDataAfterLoadedHook(useInternal);
return true;
} catch (FileNotFoundException e) {
LOG.debug("loadInfoData() the info-file wasn't found");
} catch (Exception e) {
LOG.error("loadInfoData() error during info file loading", e);
}
return false;
}
protected void loadInfoDataAfterLoadedHook(boolean useInternal) {}
protected boolean loadSliceListData(boolean useInternal) {
LOG.debug("loadSliceListData() started");
String fileName = getPathPrefix() + getNamePrefix() + "list.dat";
try (InputStream is = storage.openFile(fileName, useInternal);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
String nodes = bufferedReader.readLine();
dbRootNode = new DatabaseDataSliceNode();
dbRootNode.init(nodes, 0);
LOG.info("loadSliceListData() loaded slice list data");
return true;
} catch (Exception e) {
LOG.error("loadSliceListData() error", e);
}
return false;
}
protected abstract V getDbItemByNumberInternal(long number);
public V getDbItemByNumber(String numberString) {
LOG.info("getDbItemByNumber({}) started", numberString);
if (!isOperational()) {
LOG.warn("getDbItemByNumber() request on a non-operational DB; returning null");
return null;
}
if (numberString == null || numberString.isEmpty()) return null;
if (numberString.startsWith("+")) {
numberString = numberString.substring(1);
}
long number;
try {
number = Long.parseLong(numberString);
} catch (NumberFormatException e) {
LOG.warn("getDbItemByNumber() couldn't parse number: " + numberString, e);
return null;
}
LOG.debug("getDbItemByNumber() calling internal method for {}", number);
return getDbItemByNumberInternal(number);
}
protected abstract T createDbDataSlice();
protected T getDataSlice(long number) {
LOG.debug("getDataSlice({}) started", number);
if (number <= 0) return null;
int sliceId = dbRootNode.getSliceId(0, String.valueOf(number));
LOG.trace("getDataSlice() sliceId={}", sliceId);
if (sliceId <= 0) return null;
T slice = sliceCache.get(sliceId);
if (slice == null) {
LOG.trace("getDataSlice() loading slice with sliceId={}", sliceId);
slice = createDbDataSlice();
loadSlice(slice, sliceId);
sliceCache.put(sliceId, slice);
} else {
LOG.trace("getDataSlice() found slice in cache");
}
return slice;
}
protected void loadSlice(T slice, int sliceId) {
LOG.debug("loadSlice() started with sliceId={}", sliceId);
String fileName = getPathPrefix() + getNamePrefix() + sliceId + ".dat";
loadSlice(slice, fileName, useInternal);
}
protected void loadSlice(T slice, String fileName, boolean useInternal) {
LOG.debug("loadSlice() started with fileName={}, useInternal={}", fileName, useInternal);
try (InputStream is = storage.openFile(fileName, useInternal);
BufferedInputStream stream = new BufferedInputStream(is)) {
slice.loadFromStream(stream);
} catch (Exception e) {
LOG.error("loadSlice()", e);
}
LOG.trace("loadSlice() finished");
}
}

View File

@ -1,147 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.Arrays;
import dummydomain.yetanothercallblocker.sia.utils.LittleEndianDataInputStream;
public abstract class AbstractDatabaseDataSlice<T> {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDatabaseDataSlice.class);
protected int dbVersion;
protected int numberOfItems;
protected long[] numbers;
protected long lastAccessTimestamp;
public int getDbVersion() {
return this.dbVersion;
}
public int getNumberOfItems() {
return this.numberOfItems;
}
public long getLastAccessTimestamp() {
return this.lastAccessTimestamp;
}
protected int indexOf(long number) {
if (numberOfItems > 0) {
return Arrays.binarySearch(numbers, number);
}
return -1;
}
public T getDbItemByNumber(long number) {
// LOG.debug("getDbItemByNumber({}) started", number);
lastAccessTimestamp = System.currentTimeMillis();
int index = indexOf(number);
// LOG.trace("getDbItemByNumber() index={}", index);
if (index < 0) return null;
return getDbItemByNumberInternal(number, index);
}
protected abstract T getDbItemByNumberInternal(long number, int index);
protected void loadFromStreamCheckHeader(String header) {}
protected void loadFromStreamReadPostHeaderData(LittleEndianDataInputStream stream) throws IOException {}
protected void loadFromStreamReadPostVersionData(LittleEndianDataInputStream stream) throws IOException {}
protected void loadFromStreamInitFields() {}
protected void loadFromStreamLoadFields(int index, LittleEndianDataInputStream stream) throws IOException {}
protected void loadFromStreamLoadExtras(LittleEndianDataInputStream stream) throws IOException {
int numberOfExtras = stream.readInt();
if (numberOfExtras != 0) {
throw new IllegalStateException("Number of extras is not 0: " + numberOfExtras);
}
}
public boolean loadFromStream(BufferedInputStream inputStream) {
LOG.debug("loadFromStream() started");
try {
long currentTimeMillis = System.currentTimeMillis();
this.dbVersion = 0;
LittleEndianDataInputStream stream = new LittleEndianDataInputStream(inputStream);
LOG.trace("loadFromStream() reading header");
String headerString = stream.readUtf8StringChars(4);
loadFromStreamCheckHeader(headerString);
LOG.trace("loadFromStream() reading post header data");
loadFromStreamReadPostHeaderData(stream);
LOG.trace("loadFromStream() reading DB version");
this.dbVersion = stream.readInt();
LOG.trace("loadFromStream() DB version is {}", dbVersion);
LOG.trace("loadFromStream() reading post version data");
loadFromStreamReadPostVersionData(stream);
LOG.trace("loadFromStream() reading number of items");
this.numberOfItems = stream.readInt();
LOG.trace("loadFromStream() number of items is {}", numberOfItems);
this.numbers = new long[this.numberOfItems];
loadFromStreamInitFields();
LOG.trace("loadFromStream() reading fields");
for (int i = 0; i < this.numberOfItems; i++) {
this.numbers[i] = stream.readLong();
loadFromStreamLoadFields(i, stream);
}
LOG.trace("loadFromStream() finished reading fields");
LOG.trace("loadFromStream() reading CP");
String dividerString = stream.readUtf8StringChars(2);
if (!"CP".equalsIgnoreCase(dividerString)) {
throw new IllegalStateException("CP not found. Found instead: " + dividerString);
}
LOG.trace("loadFromStream() reading extras");
loadFromStreamLoadExtras(stream);
LOG.trace("loadFromStream() reading endmark");
String endmarkString = stream.readUtf8StringChars(6);
if (!"YABEND".equalsIgnoreCase(endmarkString) && !"MTZEND".equalsIgnoreCase(endmarkString)) {
throw new IllegalStateException("Endmark not found. Found instead: " + endmarkString);
}
LOG.debug("loadFromStream() loaded slice with {} items in {} ms",
numberOfItems, System.currentTimeMillis() - currentTimeMillis);
} catch (Exception e) {
LOG.error("loadFromStream()", e);
return false;
}
return true;
}
@Override
public String toString() {
return "AbstractDatabaseDataSlice{" +
"dbVersion=" + dbVersion +
", numberOfItems=" + numberOfItems +
", lastAccessTimestamp=" + lastAccessTimestamp +
'}';
}
}

View File

@ -1,416 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import android.annotation.SuppressLint;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.zip.GZIPInputStream;
import dummydomain.yetanothercallblocker.sia.Settings;
import dummydomain.yetanothercallblocker.sia.Storage;
import dummydomain.yetanothercallblocker.sia.network.WebService;
import dummydomain.yetanothercallblocker.sia.utils.FileUtils;
import okhttp3.MediaType;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSlice, CommunityDatabaseItem> {
private enum UpdateResult {
UPDATED, NO_UPDATES, OUTDATED_APP, BAD_SECONDARY, UNKNOWN_ERROR
}
private static final Logger LOG = LoggerFactory.getLogger(CommunityDatabase.class);
private static final int FALLBACK_APP_VERSION = 114;
private int siaAppVersion = FALLBACK_APP_VERSION;
private final String secondaryPathPrefix;
private final Settings settings;
private final WebService webService;
private SparseArray<CommunityDatabaseDataSlice> secondarySliceCache = new SparseArray<>();
@SuppressLint("UseSparseArrays") // uses null as a special value
private SparseArray<Boolean> existingSecondarySliceFiles = new SparseArray<>();
public CommunityDatabase(Storage storage, Source source,
String pathPrefix, String secondaryPathPrefix,
Settings settings, WebService webService) {
super(storage, source, pathPrefix);
this.secondaryPathPrefix = secondaryPathPrefix;
this.settings = settings;
this.webService = webService;
}
public int getEffectiveDbVersion() {
checkLoaded();
int secondaryDbVersion = settings.getSecondaryDbVersion();
return secondaryDbVersion > 0 ? secondaryDbVersion : baseDatabaseVersion;
}
public int getSiaAppVersion() {
return siaAppVersion;
}
@Override
protected String getNamePrefix() {
return "data_slice_";
}
@Override
protected void reset() {
super.reset();
siaAppVersion = FALLBACK_APP_VERSION;
secondarySliceCache.clear();
existingSecondarySliceFiles.clear();
}
@Override
protected boolean load(boolean useInternal) {
if (!super.load(useInternal)) return false;
LOG.debug("load() started; useInternal={}", useInternal);
String fileName = getPathPrefix() + "sia_info.dat";
try (InputStream is = storage.openFile(fileName, useInternal);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
if (!"SIA".equals(bufferedReader.readLine())) {
LOG.debug("load() incorrect header");
return false;
}
String appVersionString = bufferedReader.readLine();
siaAppVersion = Integer.parseInt(appVersionString);
LOG.debug("load() loaded extra info; siaAppVersion={}", this.siaAppVersion);
} catch (Exception e) {
LOG.debug("load() failed to load extra info", e);
}
return true;
}
@Override
protected void loadInfoDataAfterLoadedHook(boolean useInternal) {
int oldDbVersion = settings.getBaseDbVersion();
if (baseDatabaseVersion != oldDbVersion) {
LOG.info("loadInfoDataAfterLoadedHook() base version changed; resetting secondary DB;" +
" oldDbVersion={}, baseDatabaseVersion={}", oldDbVersion, baseDatabaseVersion);
resetSecondaryDatabase();
settings.setBaseDbVersion(baseDatabaseVersion);
}
}
@Override
protected CommunityDatabaseItem getDbItemByNumberInternal(long number) {
LOG.debug("getDbItemByNumberInternal({}) started", number);
CommunityDatabaseDataSlice secondarySlice = getSecondaryDataSlice(number);
CommunityDatabaseItem communityDatabaseItem = secondarySlice != null
? secondarySlice.getDbItemByNumber(number) : null;
if (communityDatabaseItem == null) {
LOG.trace("getDbItemByNumberInternal() not found in secondary DB");
CommunityDatabaseDataSlice baseSlice = getDataSlice(number);
communityDatabaseItem = baseSlice != null ? baseSlice.getDbItemByNumber(number) : null;
}
LOG.trace("getDbItemByNumberInternal() communityDatabaseItem={}", communityDatabaseItem);
if (communityDatabaseItem != null && !communityDatabaseItem.hasRatings()) {
communityDatabaseItem = null;
}
return communityDatabaseItem;
}
@Override
protected CommunityDatabaseDataSlice createDbDataSlice() {
return new CommunityDatabaseDataSlice();
}
private CommunityDatabaseDataSlice getSecondaryDataSlice(long number) {
LOG.debug("getSecondaryDataSlice({}) started", number);
if (number <= 0) return null;
String numberString = String.valueOf(number);
if (numberString.length() < 2) return null;
int sliceId = Integer.parseInt(numberString.substring(0, 2));
LOG.trace("getSecondaryDataSlice() sliceId={}", sliceId);
CommunityDatabaseDataSlice communityDatabaseDataSlice = secondarySliceCache.get(sliceId);
if (communityDatabaseDataSlice == null) {
LOG.trace("getSecondaryDataSlice() trying to load slice with sliceId={}", sliceId);
communityDatabaseDataSlice = new CommunityDatabaseDataSlice();
String path = getCachedSecondarySliceFilePath(sliceId);
if (path != null) {
LOG.trace("getSecondaryDataSlice() slice file exists, loading from: {}", path);
loadSlice(communityDatabaseDataSlice, path, false);
} else {
LOG.trace("getSecondaryDataSlice() slice file doesn't exist");
}
secondarySliceCache.put(sliceId, communityDatabaseDataSlice);
} else {
LOG.trace("getSecondaryDataSlice() found slice in cache");
}
return communityDatabaseDataSlice;
}
@Nullable private String getCachedSecondarySliceFilePath(int id) {
String path = getSecondarySliceFilePath(id);
Boolean exists = existingSecondarySliceFiles.get(id, null);
if (exists == null) {
exists = new File(storage.getDataDirPath() + path).exists();
existingSecondarySliceFiles.put(id, exists);
}
return exists ? path : null;
}
private String getSecondarySliceFilePath(int id) {
return getSecondaryDbPathPrefix() + id + ".sia";
}
public void resetSecondaryDatabase() {
LOG.debug("resetSecondaryDatabase() started");
File dir = new File(storage.getDataDirPath(), getSecondaryDbPathPrefix());
if (dir.exists()) {
for (File file : dir.listFiles()) {
if (!file.delete()) {
LOG.warn("resetSecondaryDatabase() failed to delete secondary DB file: {}", file.getAbsolutePath());
}
}
}
secondarySliceCache.clear();
existingSecondarySliceFiles.clear();
settings.setSecondaryDbVersion(0);
LOG.info("resetSecondaryDatabase() secondary DB was reset");
}
private String getSecondaryDbPathPrefix() {
return secondaryPathPrefix;
}
private void createSecondaryDbDirectory() {
FileUtils.createDirectory(storage.getDataDirPath(), getSecondaryDbPathPrefix());
}
public boolean updateSecondaryDb() {
LOG.info("updateSecondaryDb() started");
if (!isOperational()) {
LOG.warn("updateSecondaryDb() DB is not operational, update aborted");
return false;
}
long startTimestamp = System.currentTimeMillis();
boolean updated = false;
for (int i = 0; i < 1000; i++) {
UpdateResult result = updateSecondaryDbInternal();
LOG.debug("updateSecondaryDb() internal update result: {}", result);
if (result == UpdateResult.UPDATED) {
updated = true;
if (LOG.isTraceEnabled()) {
LOG.trace("updateSecondaryDb DB version after update: {}", getEffectiveDbVersion());
}
} else {
break;
}
}
if (updated) {
LOG.info("updateSecondaryDb() new DB version: {}", getEffectiveDbVersion());
}
LOG.info("updateSecondaryDb() finished in {} ms", System.currentTimeMillis() - startTimestamp);
return updated;
}
private UpdateResult updateSecondaryDbInternal() {
LOG.debug("updateSecondaryDbInternal() started");
long startTimestamp = System.currentTimeMillis();
int effectiveDbVersion = getEffectiveDbVersion();
LOG.debug("updateSecondaryDbInternal() effectiveDbVersion={}", effectiveDbVersion);
String dbVersionParam = "_dbVer=" + effectiveDbVersion;
String urlPath = webService.getGetDatabaseUrlPart() + "/cached?" + dbVersionParam;
try {
Response response = webService.call(urlPath, new HashMap<>());
if (response != null) {
ResponseBody body = response.body();
MediaType contentType = body.contentType();
LOG.debug("updateSecondaryDbInternal() response contentType={}", contentType);
if (contentType != null && "application".equals(contentType.type())) {
LOG.trace("updateSecondaryDbInternal() saving response data to file");
File tempFile = File.createTempFile("sia", "database",
new File(storage.getCacheDirPath()));
int totalRead = 0;
try (InputStream in = body.byteStream();
OutputStream out = new FileOutputStream(tempFile)) {
byte[] buff = new byte[10240];
while (true) {
int read = in.read(buff);
if (read == -1) {
break;
}
out.write(buff, 0, read);
totalRead += read;
}
}
LOG.trace("updateSecondaryDbInternal() finished saving response data to file; totalRead={}", totalRead);
if (totalRead > 0) {
try (FileInputStream fis = new FileInputStream(tempFile);
BufferedInputStream bis = new BufferedInputStream(new GZIPInputStream(fis))) {
LOG.trace("updateSecondaryDbInternal() loading slice from received data");
CommunityDatabaseDataSlice slice = new CommunityDatabaseDataSlice();
if (slice.loadFromStream(bis)) {
createSecondaryDbDirectory();
LOG.trace("updateSecondaryDbInternal() distributing slice");
updateSecondaryWithSlice(slice);
}
}
}
LOG.trace("updateSecondaryDbInternal() finished processing slice");
if (!tempFile.delete()) {
LOG.warn("updateSecondaryDbInternal() failed to delete tempFile {}", tempFile);
}
LOG.debug("updateSecondaryDbInternal() updated performed successfully in {} ms",
System.currentTimeMillis() - startTimestamp);
return UpdateResult.UPDATED;
} else {
String responseString = body.string();
LOG.debug("updateSecondaryDbInternal() responseString={}, elapsed time: {} ms",
responseString, System.currentTimeMillis() - startTimestamp);
responseString = responseString.replaceAll("\n", "");
switch (responseString) {
case "OAP":
LOG.trace("updateSecondaryDbInternal() server reported outdated app");
// outdated app
return UpdateResult.OUTDATED_APP;
case "NC":
LOG.trace("updateSecondaryDbInternal() server reported no updates");
// "No checkAndUpdate available" - probably "up to date"
return UpdateResult.NO_UPDATES;
case "OOD":
LOG.trace("updateSecondaryDbInternal() server suggests to reset secondary DB");
// remove secondary DB and retry
return UpdateResult.BAD_SECONDARY;
}
}
} else {
LOG.warn("updateSecondaryDbInternal() response is null");
}
} catch (IOException e) {
LOG.error("updateSecondaryDbInternal() IOE", e);
} catch (Exception e) {
LOG.error("updateSecondaryDbInternal()", e);
}
return UpdateResult.UNKNOWN_ERROR;
}
private boolean updateSecondaryWithSlice(CommunityDatabaseDataSlice dataSlice) {
LOG.debug("updateSecondaryWithSlice() started");
LOG.trace("updateSecondaryWithSlice() dataSlice={}", dataSlice);
long startTimestamp = System.currentTimeMillis();
try {
SparseArray<List<Integer>> shortSliceIdToIndexMap = new SparseArray<>();
SparseArray<List<Integer>> shortSliceIdToIndexToDeleteMap = new SparseArray<>();
dataSlice.fillIndexMaps(shortSliceIdToIndexMap, shortSliceIdToIndexToDeleteMap);
ArrayList<Integer> updatedIndexes = new ArrayList<>();
for (int sliceId = 0; sliceId <= 99; sliceId++) {
String filePath = getSecondarySliceFilePath(sliceId);
CommunityDatabaseDataSlice newSlice = new CommunityDatabaseDataSlice();
if (newSlice.partialClone(dataSlice, sliceId, shortSliceIdToIndexMap, shortSliceIdToIndexToDeleteMap)) {
CommunityDatabaseDataSlice sliceFromExistingFile = new CommunityDatabaseDataSlice();
if (getCachedSecondarySliceFilePath(sliceId) != null) {
loadSlice(sliceFromExistingFile, filePath, false);
}
try (BufferedOutputStream stream = new BufferedOutputStream(
new FileOutputStream(storage.getDataDirPath() + filePath + ".update", false))) {
sliceFromExistingFile.writeMerged(newSlice, stream);
}
updatedIndexes.add(sliceId);
LOG.debug("updateSecondaryWithSlice() added {} items to sliceId={}", newSlice.getNumberOfItems(), sliceId);
}
}
LOG.debug("updateSecondaryWithSlice() update files created, renaming files");
for (int sliceId : updatedIndexes) {
String filePath = storage.getDataDirPath() + getSecondarySliceFilePath(sliceId);
File updatedFile = new File(filePath + ".update");
File oldFile = new File(filePath);
if (oldFile.exists() && !oldFile.delete()) {
throw new IllegalStateException("Can't delete " + filePath);
}
if (!updatedFile.renameTo(oldFile)) {
throw new IllegalStateException("Can't replace slice " + updatedFile);
}
}
settings.setSecondaryDbVersion(dataSlice.getDbVersion());
secondarySliceCache.clear();
existingSecondarySliceFiles.clear();
LOG.debug("updateSecondaryWithSlice() finished in {} ms", System.currentTimeMillis() - startTimestamp);
return true;
} catch (Exception e) {
LOG.error("updateSecondaryWithSlice()", e);
return false;
}
}
}

View File

@ -1,239 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import android.util.SparseArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import dummydomain.yetanothercallblocker.sia.utils.LittleEndianDataInputStream;
import dummydomain.yetanothercallblocker.sia.utils.LittleEndianDataOutputStream;
public class CommunityDatabaseDataSlice extends AbstractDatabaseDataSlice<CommunityDatabaseItem> {
private static final Logger LOG = LoggerFactory.getLogger(CommunityDatabaseDataSlice.class);
private byte[] positiveRatingsCounts;
private byte[] negativeRatingsCounts;
private byte[] neutralRatingsCounts;
private byte[] unknownData;
private byte[] categories;
private long[] numbersToDelete;
public void fillIndexMaps(SparseArray<List<Integer>> shortSliceIdToIndexMap,
SparseArray<List<Integer>> shortSliceIdToIndexToDeleteMap) {
for (int i = 0; i <= 99; i++) {
shortSliceIdToIndexMap.put(i, new ArrayList<>());
shortSliceIdToIndexToDeleteMap.put(i, new ArrayList<>());
}
for (int i = 0; i < numbers.length; i++) {
String numberString = String.valueOf(numbers[i]);
if (numberString.length() > 1) {
int sliceId = Integer.parseInt(numberString.substring(0, 2));
shortSliceIdToIndexMap.get(sliceId).add(i);
}
}
for (int i = 0; i < numbersToDelete.length; i++) {
String numberString = String.valueOf(numbersToDelete[i]);
if (numberString.length() > 1) {
int sliceId = Integer.parseInt(numberString.substring(0, 2));
shortSliceIdToIndexToDeleteMap.get(sliceId).add(i);
}
}
}
public boolean partialClone(CommunityDatabaseDataSlice source, int shortSliceId,
SparseArray<List<Integer>> shortSliceIdToIndexMap,
SparseArray<List<Integer>> shortSliceIdToIndexToDeleteMap) {
List<Integer> numberIndexList = shortSliceIdToIndexMap.get(shortSliceId);
this.numberOfItems = numberIndexList.size();
this.dbVersion = source.dbVersion;
this.numbers = new long[this.numberOfItems];
this.positiveRatingsCounts = new byte[this.numberOfItems];
this.negativeRatingsCounts = new byte[this.numberOfItems];
this.neutralRatingsCounts = new byte[this.numberOfItems];
this.unknownData = new byte[this.numberOfItems];
this.categories = new byte[this.numberOfItems];
for (int i = 0; i < this.numberOfItems; i++) {
int index = numberIndexList.get(i);
this.numbers[i] = source.numbers[index];
this.positiveRatingsCounts[i] = source.positiveRatingsCounts[index];
this.negativeRatingsCounts[i] = source.negativeRatingsCounts[index];
this.neutralRatingsCounts[i] = source.neutralRatingsCounts[index];
this.unknownData[i] = source.unknownData[index];
this.categories[i] = source.categories[index];
}
List<Integer> numbersToDeleteIndexList = shortSliceIdToIndexToDeleteMap.get(shortSliceId);
this.numbersToDelete = new long[numbersToDeleteIndexList.size()];
for (int i = 0; i < numbersToDelete.length; i++) {
this.numbersToDelete[i] = source.numbersToDelete[numbersToDeleteIndexList.get(i)];
}
return this.numberOfItems > 0 || numbersToDelete.length > 0;
}
@Override
protected CommunityDatabaseItem getDbItemByNumberInternal(long number, int index) {
CommunityDatabaseItem communityDatabaseItem = new CommunityDatabaseItem();
communityDatabaseItem.setNumber(numbers[index]);
communityDatabaseItem.setPositiveRatingsCount(positiveRatingsCounts[index] & 255);
communityDatabaseItem.setNegativeRatingsCount(negativeRatingsCounts[index] & 255);
communityDatabaseItem.setNeutralRatingsCount(neutralRatingsCounts[index] & 255);
communityDatabaseItem.setUnknownData(unknownData[index] & 255);
communityDatabaseItem.setCategory(categories[index] & 255);
return communityDatabaseItem;
}
@Override
protected void loadFromStreamCheckHeader(String header) {
if (!"YABF".equalsIgnoreCase(header) && !"MTZF".equalsIgnoreCase(header)
&& !"MTZD".equalsIgnoreCase(header)) {
throw new IllegalStateException("Invalid header. Actual value: " + header);
}
}
@Override
protected void loadFromStreamReadPostHeaderData(LittleEndianDataInputStream stream) throws IOException {
byte b = stream.readByte(); // ignored
LOG.trace("loadFromStreamReadPostHeaderData() b={}", b);
}
@Override
protected void loadFromStreamReadPostVersionData(LittleEndianDataInputStream stream) throws IOException {
String s = stream.readUtf8StringChars(2); // ignored
int i = stream.readInt(); // ignored
LOG.trace("loadFromStreamReadPostVersionData() s={}, i={}", s, i);
}
@Override
protected void loadFromStreamInitFields() {
positiveRatingsCounts = new byte[numberOfItems];
negativeRatingsCounts = new byte[numberOfItems];
neutralRatingsCounts = new byte[numberOfItems];
unknownData = new byte[numberOfItems];
categories = new byte[numberOfItems];
}
@Override
protected void loadFromStreamLoadFields(int index, LittleEndianDataInputStream stream) throws IOException {
positiveRatingsCounts[index] = stream.readByte();
negativeRatingsCounts[index] = stream.readByte();
neutralRatingsCounts[index] = stream.readByte();
unknownData[index] = stream.readByte();
categories[index] = stream.readByte();
}
@Override
protected void loadFromStreamLoadExtras(LittleEndianDataInputStream stream) throws IOException {
int numberOfItemsToDelete = stream.readInt();
LOG.trace("loadFromStreamLoadExtras() numberOfItemsToDelete={}", numberOfItemsToDelete);
numbersToDelete = new long[numberOfItemsToDelete];
for (int i = 0; i < numberOfItemsToDelete; i++) {
numbersToDelete[i] = stream.readLong();
}
}
public boolean writeMerged(CommunityDatabaseDataSlice newSlice, BufferedOutputStream outputStream) {
LOG.debug("writeMerged() started with newSlice={}", newSlice);
try {
int realNumberOfItems = 0;
int newNumberOfItems = this.numberOfItems;
if (newSlice != null) {
if (this.numberOfItems > 0) {
for (long n : newSlice.numbers) {
if (indexOf(n) < 0) {
newNumberOfItems++;
}
}
for (long n : newSlice.numbersToDelete) {
if (indexOf(n) < 0) {
newNumberOfItems++;
}
}
} else {
newNumberOfItems = newSlice.numberOfItems + newSlice.numbersToDelete.length;
}
}
LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(outputStream);
stream.writeUtf8StringChars("YABF");
stream.writeByte((byte) 1);
stream.writeInt(newSlice != null ? newSlice.dbVersion : this.dbVersion);
stream.writeUtf8StringChars("ww");
stream.writeInt(0);
stream.writeInt(newNumberOfItems);
if (this.numberOfItems >= 0) {
int sourceIndex = 0;
int newIndex = 0;
int newDeletedIndex = 0;
do {
long sourceNumber = sourceIndex < this.numberOfItems ? this.numbers[sourceIndex] : Long.MAX_VALUE;
boolean replacesSourceNumber = false;
if (newSlice != null) {
while (true) {
long newNumber = (newIndex >= newSlice.numbers.length || newSlice.numbers[newIndex] > sourceNumber) ? 0 : newSlice.numbers[newIndex];
long newDeletedNumber = (newDeletedIndex >= newSlice.numbersToDelete.length || newSlice.numbersToDelete[newDeletedIndex] > sourceNumber) ? 0 : newSlice.numbersToDelete[newDeletedIndex];
if (newNumber <= 0 || (newDeletedNumber != 0 && newNumber >= newDeletedNumber)) {
if (newDeletedNumber <= 0) {
break;
}
stream.writeLong(newSlice.numbersToDelete[newDeletedIndex]);
byte zero = (byte) 0;
stream.writeByte(zero);
stream.writeByte(zero);
stream.writeByte(zero);
stream.writeByte(zero);
stream.writeByte(zero);
replacesSourceNumber = newSlice.numbersToDelete[newDeletedIndex] == sourceNumber;
newDeletedIndex++;
realNumberOfItems++;
} else {
stream.writeLong(newSlice.numbers[newIndex]);
stream.writeByte(newSlice.positiveRatingsCounts[newIndex]);
stream.writeByte(newSlice.negativeRatingsCounts[newIndex]);
stream.writeByte(newSlice.neutralRatingsCounts[newIndex]);
stream.writeByte(newSlice.unknownData[newIndex]);
stream.writeByte(newSlice.categories[newIndex]);
replacesSourceNumber = newSlice.numbers[newIndex] == sourceNumber;
newIndex++;
realNumberOfItems++;
}
}
}
if (!replacesSourceNumber && sourceIndex < this.numberOfItems) {
stream.writeLong(this.numbers[sourceIndex]);
stream.writeByte(this.positiveRatingsCounts[sourceIndex]);
stream.writeByte(this.negativeRatingsCounts[sourceIndex]);
stream.writeByte(this.neutralRatingsCounts[sourceIndex]);
stream.writeByte(this.unknownData[sourceIndex]);
stream.writeByte(this.categories[sourceIndex]);
realNumberOfItems++;
}
} while (sourceIndex++ != this.numberOfItems);
}
if (realNumberOfItems != newNumberOfItems) {
LOG.error("writeMerged() realNumberOfItems={}, newNumberOfItems={}, dbVersion={}, newSlice.dbVersion={}",
realNumberOfItems, newNumberOfItems, dbVersion, newSlice != null ? newSlice.dbVersion : 0);
throw new IllegalStateException("writeMerged results in invalid realNumberOfItems!");
}
stream.writeUtf8StringChars("CP");
stream.writeInt(0);
stream.writeUtf8StringChars("YABEND");
return true;
} catch (IOException e) {
LOG.error("writeMerged()", e);
return false;
}
}
}

View File

@ -1,81 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import dummydomain.yetanothercallblocker.sia.model.NumberCategory;
public class CommunityDatabaseItem {
private long number;
private int positiveRatingsCount;
private int negativeRatingsCount;
private int neutralRatingsCount;
private int unknownData;
private int category;
public long getNumber() {
return number;
}
public void setNumber(long j) {
this.number = j;
}
public int getPositiveRatingsCount() {
return this.positiveRatingsCount;
}
public void setPositiveRatingsCount(int c) {
this.positiveRatingsCount = c;
}
public int getNegativeRatingsCount() {
return this.negativeRatingsCount;
}
public void setNegativeRatingsCount(int c) {
this.negativeRatingsCount = c;
}
public int getNeutralRatingsCount() {
return this.neutralRatingsCount;
}
public void setNeutralRatingsCount(int c) {
this.neutralRatingsCount = c;
}
public int getUnknownData() {
return unknownData;
}
public void setUnknownData(int v) {
this.unknownData = v;
}
public int getCategory() {
return this.category;
}
public void setCategory(int c) {
this.category = c;
}
public boolean hasRatings() {
return positiveRatingsCount + negativeRatingsCount + negativeRatingsCount > 0;
}
@Override
public String toString() {
return "CommunityDatabaseItem{" +
"number=" + number +
", positiveRatingsCount=" + positiveRatingsCount +
", negativeRatingsCount=" + negativeRatingsCount +
", neutralRatingsCount=" + neutralRatingsCount +
", unknownData=" + unknownData +
", category=" + NumberCategory.getById(category) + " (" + category + ")" +
'}';
}
}

View File

@ -1,43 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
class DatabaseDataSliceNode {
private DatabaseDataSliceNode[] sliceTree;
public int init(String data, int offset) {
this.sliceTree = new DatabaseDataSliceNode[10];
for (int i = 0; i <= 9; i++) {
if (data.charAt(offset) == '*' || data.charAt(offset) == '+') {
offset++;
continue;
}
DatabaseDataSliceNode node = new DatabaseDataSliceNode();
offset = node.init(data, offset + 1);
sliceTree[i] = node;
}
return offset;
}
public int getSliceId(int currentNumber, String number) {
int currentDigit = Integer.parseInt(number.substring(0, 1));
number = number.substring(1);
currentNumber = currentNumber * 10 + currentDigit;
if (sliceTree[currentDigit] == null) {
return currentNumber;
}
if (number.length() > 0) {
DatabaseDataSliceNode sliceNode = sliceTree[currentDigit];
return sliceNode.getSliceId(currentNumber, number);
}
// the number is shorter
return 0;
}
}

View File

@ -1,68 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import dummydomain.yetanothercallblocker.sia.Storage;
import dummydomain.yetanothercallblocker.sia.network.DbDownloader;
import dummydomain.yetanothercallblocker.sia.utils.FileUtils;
public class DbManager {
private static final Logger LOG = LoggerFactory.getLogger(DbManager.class);
private static final String DEFAULT_URL = "https://gitlab.com/xynngh/YetAnotherCallBlocker_data/raw/zip_v1/archives/sia.zip";
private final Storage storage;
private final String pathPrefix;
public DbManager(Storage storage, String pathPrefix) {
this.storage = storage;
this.pathPrefix = pathPrefix;
}
public boolean downloadMainDb() {
return downloadMainDb(DEFAULT_URL);
}
public boolean downloadMainDb(String url) {
LOG.debug("downloadMainDb() started");
File dataDir = new File(storage.getDataDirPath());
String siaDir = pathPrefix;
String tmpUpdateDir = siaDir.substring(0, siaDir.indexOf('/')) + "-tmp/";
String oldDir = siaDir.substring(0, siaDir.indexOf('/')) + "-old/";
FileUtils.delete(dataDir, tmpUpdateDir);
FileUtils.createDirectory(dataDir, tmpUpdateDir);
LOG.debug("downloadMainDb() prepared dirs");
if (DbDownloader.download(url, storage.getDataDirPath() + tmpUpdateDir)) {
LOG.debug("downloadMainDb() downloaded and unpacked");
File old = new File(dataDir, siaDir);
if (old.exists() && !old.renameTo(new File(dataDir, oldDir))) {
LOG.warn("downloadMainDb() couldn't rename sia to old");
return false;
}
if (!new File(dataDir, tmpUpdateDir).renameTo(new File(dataDir, siaDir))) {
LOG.warn("downloadMainDb() couldn't rename tmp to sia");
return false;
}
FileUtils.delete(dataDir, oldDir);
LOG.debug("downloadMainDb() folders moved");
return true;
} else {
LOG.warn("downloadMainDb() failed downloading");
}
return false;
}
}

View File

@ -1,27 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import dummydomain.yetanothercallblocker.sia.Storage;
public class FeaturedDatabase extends AbstractDatabase<FeaturedDatabaseDataSlice, FeaturedDatabaseItem> {
public FeaturedDatabase(Storage storage, Source source, String pathPrefix) {
super(storage, source, pathPrefix);
}
@Override
protected String getNamePrefix() {
return "featured_slice_";
}
@Override
protected FeaturedDatabaseItem getDbItemByNumberInternal(long number) {
FeaturedDatabaseDataSlice slice = getDataSlice(number);
return slice != null ? slice.getDbItemByNumber(number) : null;
}
@Override
protected FeaturedDatabaseDataSlice createDbDataSlice() {
return new FeaturedDatabaseDataSlice();
}
}

View File

@ -1,34 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
import java.io.IOException;
import dummydomain.yetanothercallblocker.sia.utils.LittleEndianDataInputStream;
public class FeaturedDatabaseDataSlice extends AbstractDatabaseDataSlice<FeaturedDatabaseItem> {
private String[] names;
@Override
protected FeaturedDatabaseItem getDbItemByNumberInternal(long number, int index) {
return new FeaturedDatabaseItem(number, names[index]);
}
@Override
protected void loadFromStreamCheckHeader(String header) {
if (!"YABX".equalsIgnoreCase(header) && !"MTZX".equalsIgnoreCase(header)) {
throw new IllegalStateException("Invalid header. Actual value: " + header);
}
}
@Override
protected void loadFromStreamInitFields() {
names = new String[numberOfItems];
}
@Override
protected void loadFromStreamLoadFields(int index, LittleEndianDataInputStream stream) throws IOException {
int nameLength = stream.readInt();
names[index] = stream.readUtf8StringChars(nameLength);
}
}

View File

@ -1,38 +0,0 @@
package dummydomain.yetanothercallblocker.sia.model.database;
public class FeaturedDatabaseItem {
private long number;
private String name;
public FeaturedDatabaseItem(long number, String name) {
this.number = number;
this.name = name;
}
public long getNumber() {
return number;
}
public void setNumber(long number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "FeaturedDatabaseItem{" +
"number=" + number +
", name='" + name + '\'' +
'}';
}
}

View File

@ -1,61 +0,0 @@
package dummydomain.yetanothercallblocker.sia.network;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import dummydomain.yetanothercallblocker.sia.utils.FileUtils;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Okio;
import okio.Sink;
public class DbDownloader {
private static final Logger LOG = LoggerFactory.getLogger(DbDownloader.class);
public static boolean download(String url, String path) {
LOG.info("download() started; path: {}", path);
LOG.debug("download() making a request");
Request request = new Request.Builder().url(url).build();
try (Response response = new OkHttpClient().newCall(request).execute()) {
LOG.debug("download() got response; successful: {}", response.isSuccessful());
if (response.isSuccessful()) {
ResponseBody body = response.body();
try (ZipInputStream zipInputStream = new ZipInputStream(body.byteStream())) {
for (ZipEntry zipEntry; (zipEntry = zipInputStream.getNextEntry()) != null; ) {
String name = zipEntry.getName();
if (zipEntry.isDirectory()) {
FileUtils.createDirectory(path + name);
continue;
}
try (Sink out = Okio.sink(new File(path + name))) {
Okio.buffer(Okio.source(zipInputStream)).readAll(out);
}
}
LOG.debug("download() finished successfully");
return true;
}
} else {
LOG.warn("download() unsuccessful response {}", response.message());
}
} catch (IOException e) {
LOG.warn("download()", e);
}
LOG.debug("download() finished unsuccessfully");
return false;
}
}

View File

@ -1,206 +0,0 @@
package dummydomain.yetanothercallblocker.sia.network;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import dummydomain.yetanothercallblocker.sia.utils.Utils;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class WebService {
public interface WSParameterProvider {
String getAppId();
String getDevice();
String getModel();
String getManufacturer();
int getAndroidApi();
String getAppFamily();
int getAppVersion();
int getDbVersion();
String getCountry();
}
public static abstract class DefaultWSParameterProvider implements WSParameterProvider {
@Override
public String getDevice() {
return "dreamlte";
}
@Override
public String getModel() {
return "SM-G950F";
}
@Override
public String getManufacturer() {
return "Samsung";
}
@Override
public int getAndroidApi() {
return 26;
}
@Override
public String getAppFamily() {
return "SIA-NEXT";
}
@Override
public String getCountry() {
return "US";
}
}
public static class WSResponse {
private boolean successful;
private int responseCode;
private String responseMessage;
private JSONObject jsonObject;
public void setSuccessful(boolean successful) {
this.successful = successful;
}
public boolean getSuccessful() {
return this.successful;
}
public void setResponseCode(int i) {
this.responseCode = i;
}
public void setResponseMessage(String str) {
this.responseMessage = str;
}
public String getResponseMessage() {
return this.responseMessage;
}
public void setJsonObject(JSONObject jSONObject) {
this.jsonObject = jSONObject;
}
public final JSONObject getJsonObject() {
return this.jsonObject;
}
}
private static final Logger LOG = LoggerFactory.getLogger(WebService.class);
private static final String GET_DATABASE_URL_PART = "/get-database";
private static final String GET_REVIEWS_URL_PART = "/get-reviews";
private final WSParameterProvider parameterProvider;
public WebService(WSParameterProvider parameterProvider) {
this.parameterProvider = parameterProvider;
}
public String getGetDatabaseUrlPart() {
return GET_DATABASE_URL_PART;
}
public String getGetReviewsUrlPart() {
return GET_REVIEWS_URL_PART;
}
public Response call(String path, Map<String, String> params) {
LOG.debug("call() started; path={}", path);
// TODO: retries
Request request = new Request.Builder().url(addHostname(path)).post(createRequestBody(params)).build();
try {
return new OkHttpClient().newCall(request).execute();
} catch (IOException e) {
LOG.warn("call()", e);
}
return null;
}
public WSResponse callForJson(String path, Map<String, String> params) {
LOG.debug("callForJson() started; path={}", path);
try {
Response response = call(path, params);
if (response != null) {
WSResponse wsResponse = new WSResponse();
if (response.isSuccessful()) {
ResponseBody body = response.body();
if (body != null) {
String bodyString = body.string();
LOG.trace("callForJson() response body: {}", bodyString);
try {
wsResponse.setJsonObject(new JSONObject(bodyString));
JSONObject c = wsResponse.getJsonObject();
wsResponse.setSuccessful(c.getBoolean("success"));
} catch (Exception e) {
LOG.error("callForJson()", e);
}
}
} else {
LOG.trace("callForJson() response is not successful");
wsResponse.setResponseCode(response.code());
wsResponse.setResponseMessage(response.message());
response.close();
}
return wsResponse;
} else {
LOG.warn("callForJson() response is null");
}
} catch (Exception e) {
LOG.warn("callForJson()", e);
}
return null;
}
private String addHostname(String path) {
return "https://aapi.shouldianswer.net/srvapp" + path;
}
private RequestBody createRequestBody(Map<String, String> params) {
if (params == null) params = new HashMap<>();
params.put("_appId", parameterProvider.getAppId());
params.put("_device", parameterProvider.getDevice());
params.put("_model", parameterProvider.getModel());
params.put("_manufacturer", parameterProvider.getManufacturer());
params.put("_api", String.valueOf(parameterProvider.getAndroidApi()));
params.put("_appFamily", parameterProvider.getAppFamily());
params.put("_appVer", String.valueOf(parameterProvider.getAppVersion()));
params.put("_dbVer", String.valueOf(parameterProvider.getDbVersion()));
params.put("_country", parameterProvider.getCountry());
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
StringBuilder paramsSb = new StringBuilder();
for (String key : params.keySet()) {
String val = params.get(key);
if (val != null) {
paramsSb.append(val);
bodyBuilder.addFormDataPart(key, val);
}
}
bodyBuilder.addFormDataPart("_checksum", Utils.md5String(paramsSb.toString() + "saltandmira2"));
return bodyBuilder.build();
}
}

View File

@ -1,53 +0,0 @@
package dummydomain.yetanothercallblocker.sia.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class FileUtils {
private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class);
public static File createDirectory(String path) {
return createDirectory(new File(path));
}
public static File createDirectory(String base, String dir) {
return createDirectory(new File(base, dir));
}
public static File createDirectory(File base, String dir) {
return createDirectory(new File(base, dir));
}
public static File createDirectory(File d) {
if (!d.exists()) {
LOG.debug("createDirectory() creating: {}, result: {}", d.getAbsolutePath(), d.mkdir());
}
if (!d.isDirectory()) {
LOG.error("createDirectory() is not a directory: {}", d.getAbsolutePath());
}
return d;
}
public static void delete(File base, String name) {
delete(new File(base, name));
}
public static void delete(File file) {
if (file.exists()) {
if (file.isDirectory()) {
File[] list = file.listFiles();
if (list != null) {
for (File f : list) {
delete(f);
}
}
} else {
file.delete();
}
}
}
}

View File

@ -1,60 +0,0 @@
package dummydomain.yetanothercallblocker.sia.utils;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
public class LittleEndianDataInputStream {
private InputStream in;
private byte[] buffer;
public LittleEndianDataInputStream(InputStream in) {
this(in, 200);
}
public LittleEndianDataInputStream(InputStream in, int bufferSize) {
this.in = in;
buffer = new byte[bufferSize];
}
public byte readByte() throws IOException {
int ch = in.read();
if (ch < 0) throw new EOFException();
return (byte)(ch);
}
public int readInt() throws IOException {
if (in.read(buffer, 0, 4) == 4) {
return (((buffer[0] & 0xff) | ((buffer[1] & 0xff) << 8))
| ((buffer[2] & 0xff) << 16)) | ((buffer[3] & 0xff) << 24);
}
throw new EOFException();
}
public long readLong() throws IOException {
if (in.read(buffer, 0, 8) == 8) {
return (((((((((((((((long) (buffer[7] & 0xff)) << 8)
| ((long) (buffer[6] & 0xff))) << 8)
| ((long) (buffer[5] & 0xff))) << 8)
| ((long) (buffer[4] & 0xff))) << 8)
| ((long) (buffer[3] & 0xff))) << 8)
| ((long) (buffer[2] & 0xff))) << 8)
| ((long) (buffer[1] & 0xff))) << 8)
| ((long) (buffer[0] & 0xff));
}
throw new EOFException();
}
public String readUtf8StringChars(int length) throws IOException {
if (buffer.length < length) buffer = new byte[length];
if (in.read(buffer, 0, length) == length) {
return new String(buffer, 0, length, Charset.forName("UTF-8"));
}
throw new EOFException();
}
}

View File

@ -1,51 +0,0 @@
package dummydomain.yetanothercallblocker.sia.utils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
public class LittleEndianDataOutputStream {
private OutputStream out;
private byte[] buffer;
public LittleEndianDataOutputStream(OutputStream out) {
this(out, 8);
}
public LittleEndianDataOutputStream(OutputStream out, int bufferSize) {
this.out = out;
buffer = new byte[bufferSize];
}
public void writeByte(byte b) throws IOException {
out.write(b);
}
public void writeInt(int i) throws IOException {
buffer[0] = (byte) (i & 0xff);
buffer[1] = (byte) ((i >>> 8) & 0xff);
buffer[2] = (byte) ((i >>> 16) & 0xff);
buffer[3] = (byte) ((i >>> 24) & 0xff);
out.write(buffer, 0, 4);
}
public void writeLong(long l) throws IOException {
buffer[0] = (byte) (l & 0xff);
buffer[1] = (byte) ((l >>> 8) & 0xff);
buffer[2] = (byte) ((l >>> 16) & 0xff);
buffer[3] = (byte) ((l >>> 24) & 0xff);
buffer[4] = (byte) ((l >>> 32) & 0xff);
buffer[5] = (byte) ((l >>> 40) & 0xff);
buffer[6] = (byte) ((l >>> 48) & 0xff);
buffer[7] = (byte) ((l >>> 56) & 0xff);
out.write(buffer, 0, 8);
}
public void writeUtf8StringChars(String str) throws IOException {
byte[] bytes = str.getBytes(Charset.forName("UTF-8"));
out.write(bytes, 0, bytes.length);
}
}

View File

@ -1,30 +0,0 @@
package dummydomain.yetanothercallblocker.sia.utils;
import android.util.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import java.nio.ByteBuffer;
import java.util.UUID;
public class Utils {
public static String md5String(String str) {
return new String(Hex.encodeHex(DigestUtils.md5(str.getBytes())));
}
public static String generateAppId() {
UUID randomUUID = UUID.randomUUID();
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.putLong(randomUUID.getMostSignificantBits());
buffer.putLong(randomUUID.getLeastSignificantBits());
return Base64.encodeToString(buffer.array(), Base64.NO_PADDING)
.replace('/', '_')
.replace('\\', '-')
.replaceAll("\n", "");
}
}

View File

@ -12,6 +12,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}