mirror of
https://gitlab.com/xynngh/YetAnotherCallBlocker.git
synced 2025-02-16 20:00:35 +01:00
Add support for external main DB
This commit is contained in:
parent
f8403c3771
commit
375278d998
@ -1,12 +1,19 @@
|
|||||||
package dummydomain.yetanothercallblocker;
|
package dummydomain.yetanothercallblocker;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
import androidx.appcompat.widget.SwitchCompat;
|
||||||
|
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import dummydomain.yetanothercallblocker.sia.DatabaseSingleton;
|
||||||
|
import dummydomain.yetanothercallblocker.sia.model.database.DbManager;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@ -32,6 +39,20 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
else Updater.cancelAutoUpdateWorker();
|
else Updater.cancelAutoUpdateWorker();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
AsyncTask<Void, Void, Boolean> noDbTask = new AsyncTask<Void, Void, Boolean>() {
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... voids) {
|
||||||
|
return DatabaseSingleton.getCommunityDatabase().isOperational();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
updateNoDbUi(result ? UpdateUiState.HIDDEN : UpdateUiState.NO_DB);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
noDbTask.execute();
|
||||||
|
|
||||||
PermissionHelper.checkPermissions(this);
|
PermissionHelper.checkPermissions(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +70,41 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// TODO: handle
|
// TODO: handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onDownloadDbClick(View view) {
|
||||||
|
// TODO: use service
|
||||||
|
|
||||||
|
updateNoDbUi(UpdateUiState.DOWNLOADING_DB);
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
AsyncTask<Void, Void, Boolean> dlTask = new AsyncTask<Void, Void, Boolean>() {
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... voids) {
|
||||||
|
return DbManager.downloadMainDb()
|
||||||
|
&& DatabaseSingleton.getCommunityDatabase().reload()
|
||||||
|
&& DatabaseSingleton.getFeaturedDatabase().reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
updateNoDbUi(result ? UpdateUiState.HIDDEN : UpdateUiState.ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dlTask.execute();
|
||||||
|
}
|
||||||
|
|
||||||
public void onOpenDebugActivity(MenuItem item) {
|
public void onOpenDebugActivity(MenuItem item) {
|
||||||
startActivity(new Intent(this, DebugActivity.class));
|
startActivity(new Intent(this, DebugActivity.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UpdateUiState {HIDDEN, NO_DB, DOWNLOADING_DB, ERROR}
|
||||||
|
|
||||||
|
private void updateNoDbUi(UpdateUiState state) {
|
||||||
|
findViewById(R.id.noDbText).setVisibility(state == UpdateUiState.NO_DB ? View.VISIBLE : View.GONE);
|
||||||
|
findViewById(R.id.downloadDbButton).setVisibility(state == UpdateUiState.NO_DB ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
findViewById(R.id.downloadingDbText).setVisibility(state == UpdateUiState.DOWNLOADING_DB ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
findViewById(R.id.dbCouldNotBeDownloaded).setVisibility(state == UpdateUiState.ERROR ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@ package dummydomain.yetanothercallblocker.sia;
|
|||||||
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabase;
|
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabase;
|
||||||
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabase;
|
import dummydomain.yetanothercallblocker.sia.model.database.FeaturedDatabase;
|
||||||
|
|
||||||
|
import static dummydomain.yetanothercallblocker.sia.SiaConstants.*;
|
||||||
|
|
||||||
public class DatabaseSingleton {
|
public class DatabaseSingleton {
|
||||||
|
|
||||||
private static final CommunityDatabase COMMUNITY_DATABASE = new CommunityDatabase();
|
private static final CommunityDatabase COMMUNITY_DATABASE = new CommunityDatabase(SIA_PATH_PREFIX, SIA_SECONDARY_PATH_PREFIX);
|
||||||
private static final FeaturedDatabase FEATURED_DATABASE = new FeaturedDatabase();
|
private static final FeaturedDatabase FEATURED_DATABASE = new FeaturedDatabase(SIA_PATH_PREFIX);
|
||||||
|
|
||||||
public static CommunityDatabase getCommunityDatabase() {
|
public static CommunityDatabase getCommunityDatabase() {
|
||||||
return COMMUNITY_DATABASE;
|
return COMMUNITY_DATABASE;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package dummydomain.yetanothercallblocker.sia;
|
||||||
|
|
||||||
|
public interface SiaConstants {
|
||||||
|
|
||||||
|
String SIA_PATH_PREFIX = "sia/";
|
||||||
|
String SIA_SECONDARY_PATH_PREFIX = "sia-secondary/";
|
||||||
|
|
||||||
|
}
|
@ -5,17 +5,22 @@ import android.util.SparseArray;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
|
||||||
import dummydomain.yetanothercallblocker.sia.utils.Utils;
|
import static dummydomain.yetanothercallblocker.sia.utils.FileUtils.openFile;
|
||||||
|
|
||||||
public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V> {
|
public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractDatabase.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractDatabase.class);
|
||||||
|
|
||||||
|
protected final String pathPrefix;
|
||||||
|
|
||||||
|
protected boolean useAssets;
|
||||||
|
|
||||||
protected int baseDatabaseVersion;
|
protected int baseDatabaseVersion;
|
||||||
protected int numberOfItems;
|
protected int numberOfItems;
|
||||||
|
|
||||||
@ -26,39 +31,78 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
protected boolean loaded;
|
protected boolean loaded;
|
||||||
protected boolean failedToLoad;
|
protected boolean failedToLoad;
|
||||||
|
|
||||||
|
public AbstractDatabase(String pathPrefix) {
|
||||||
|
this.pathPrefix = pathPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOperational() {
|
||||||
|
checkLoaded();
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean reload() {
|
||||||
|
return load();
|
||||||
|
}
|
||||||
|
|
||||||
protected void checkLoaded() {
|
protected void checkLoaded() {
|
||||||
if (loaded) return;
|
if (loaded || failedToLoad) return;
|
||||||
if (failedToLoad) throw new IllegalStateException("The DB has failed to load");
|
|
||||||
|
|
||||||
LOG.debug("checkLoaded() loading DB");
|
LOG.debug("checkLoaded() loading DB");
|
||||||
loaded = load();
|
load();
|
||||||
failedToLoad = !loaded;
|
}
|
||||||
|
|
||||||
|
protected void reset() {
|
||||||
|
loaded = false;
|
||||||
|
failedToLoad = false;
|
||||||
|
|
||||||
|
baseDatabaseVersion = 0;
|
||||||
|
numberOfItems = 0;
|
||||||
|
|
||||||
|
dbRootNode = null;
|
||||||
|
sliceCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean load() {
|
protected boolean load() {
|
||||||
LOG.debug("load() loading DB");
|
LOG.debug("load() loading DB");
|
||||||
|
|
||||||
if (loadInfoData() && loadSliceListData()) {
|
reset();
|
||||||
LOG.info("load() loaded DB, baseDatabaseVersion={}, numberOfItems={}",
|
|
||||||
this.baseDatabaseVersion, this.numberOfItems);
|
if (load(true)) {
|
||||||
|
useAssets = true;
|
||||||
|
loaded = true;
|
||||||
|
return true;
|
||||||
|
} else if (load(false)) {
|
||||||
|
useAssets = false;
|
||||||
|
loaded = true;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("load() failed to load DB");
|
LOG.warn("load() failed to load DB");
|
||||||
|
failedToLoad = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getAssetPathPrefix() {
|
protected boolean load(boolean useAssets) {
|
||||||
return "sia/";
|
if (loadInfoData(useAssets) && loadSliceListData(useAssets)) {
|
||||||
|
LOG.info("load() loaded DB useAssets={}, baseDatabaseVersion={}, numberOfItems={}",
|
||||||
|
useAssets, this.baseDatabaseVersion, this.numberOfItems);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract String getAssetNamePrefix();
|
protected String getPathPrefix() {
|
||||||
|
return pathPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean loadInfoData() {
|
protected abstract String getNamePrefix();
|
||||||
LOG.debug("loadInfoData() started");
|
|
||||||
|
|
||||||
String fileName = getAssetPathPrefix() + getAssetNamePrefix() + "info.dat";
|
protected boolean loadInfoData(boolean useAssets) {
|
||||||
try (InputStream is = Utils.getContext().getAssets().open(fileName);
|
LOG.debug("loadInfoData() started; useAssets: {}", useAssets);
|
||||||
|
|
||||||
|
String fileName = getPathPrefix() + getNamePrefix() + "info.dat";
|
||||||
|
|
||||||
|
try (InputStream is = openFile(fileName, useAssets);
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
|
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
|
||||||
|
|
||||||
String headerString = bufferedReader.readLine();
|
String headerString = bufferedReader.readLine();
|
||||||
@ -81,11 +125,11 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
LOG.debug("loadInfoData() loaded MDI, baseDatabaseVersion={}, numberOfItems={}",
|
LOG.debug("loadInfoData() loaded MDI, baseDatabaseVersion={}, numberOfItems={}",
|
||||||
this.baseDatabaseVersion, this.numberOfItems);
|
this.baseDatabaseVersion, this.numberOfItems);
|
||||||
|
|
||||||
loadInfoDataAfterLoadedHook();
|
loadInfoDataAfterLoadedHook(useAssets);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
LOG.error("loadInfoData() the info-file is not found! HAVE YOU COPIED THE ASSETS? (see `Building` in README)", e);
|
LOG.debug("loadInfoData() the info-file wasn't found");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("loadInfoData() error during info file loading", e);
|
LOG.error("loadInfoData() error during info file loading", e);
|
||||||
}
|
}
|
||||||
@ -93,13 +137,13 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void loadInfoDataAfterLoadedHook() {}
|
protected void loadInfoDataAfterLoadedHook(boolean useAssets) {}
|
||||||
|
|
||||||
protected boolean loadSliceListData() {
|
protected boolean loadSliceListData(boolean useAssets) {
|
||||||
LOG.debug("loadSliceListData() started");
|
LOG.debug("loadSliceListData() started");
|
||||||
|
|
||||||
String fileName = getAssetPathPrefix() + getAssetNamePrefix() + "list.dat";
|
String fileName = getPathPrefix() + getNamePrefix() + "list.dat";
|
||||||
try (InputStream is = Utils.getContext().getAssets().open(fileName);
|
try (InputStream is = openFile(fileName, useAssets);
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
|
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
|
||||||
String nodes = bufferedReader.readLine();
|
String nodes = bufferedReader.readLine();
|
||||||
|
|
||||||
@ -120,6 +164,11 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
public V getDbItemByNumber(String numberString) {
|
public V getDbItemByNumber(String numberString) {
|
||||||
LOG.info("getDbItemByNumber({}) started", 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 == null || numberString.isEmpty()) return null;
|
||||||
|
|
||||||
if (numberString.startsWith("+")) {
|
if (numberString.startsWith("+")) {
|
||||||
@ -134,8 +183,6 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLoaded();
|
|
||||||
|
|
||||||
LOG.debug("getDbItemByNumber() calling internal method for {}", number);
|
LOG.debug("getDbItemByNumber() calling internal method for {}", number);
|
||||||
return getDbItemByNumberInternal(number);
|
return getDbItemByNumberInternal(number);
|
||||||
}
|
}
|
||||||
@ -156,7 +203,7 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
LOG.trace("getDataSlice() loading slice with sliceId={}", sliceId);
|
LOG.trace("getDataSlice() loading slice with sliceId={}", sliceId);
|
||||||
|
|
||||||
slice = createDbDataSlice();
|
slice = createDbDataSlice();
|
||||||
slice.loadFromAsset(Utils.getContext(), sliceId);
|
loadSlice(slice, sliceId);
|
||||||
|
|
||||||
sliceCache.put(sliceId, slice);
|
sliceCache.put(sliceId, slice);
|
||||||
} else {
|
} else {
|
||||||
@ -165,4 +212,24 @@ public abstract class AbstractDatabase<T extends AbstractDatabaseDataSlice<V>, V
|
|||||||
return slice;
|
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, useAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void loadSlice(T slice, String fileName, boolean useAssets) {
|
||||||
|
LOG.debug("loadSlice() started with fileName={}, useAssets={}", fileName, useAssets);
|
||||||
|
|
||||||
|
try (InputStream is = openFile(fileName, useAssets);
|
||||||
|
BufferedInputStream stream = new BufferedInputStream(is)) {
|
||||||
|
|
||||||
|
slice.loadFromStream(stream);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("loadSlice()", e);
|
||||||
|
}
|
||||||
|
LOG.trace("loadSlice() finished");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
package dummydomain.yetanothercallblocker.sia.model.database;
|
package dummydomain.yetanothercallblocker.sia.model.database;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import dummydomain.yetanothercallblocker.sia.utils.LittleEndianDataInputStream;
|
import dummydomain.yetanothercallblocker.sia.utils.LittleEndianDataInputStream;
|
||||||
@ -139,41 +135,6 @@ public abstract class AbstractDatabaseDataSlice<T> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getAssetPathPrefix() {
|
|
||||||
return "sia/";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract String getAssetNamePrefix();
|
|
||||||
|
|
||||||
public void loadFromAsset(Context context, int sliceId) {
|
|
||||||
LOG.debug("loadFromAsset() started with sliceId={}", sliceId);
|
|
||||||
|
|
||||||
String assetName = getAssetPathPrefix() + getAssetNamePrefix() + String.valueOf(sliceId) + ".dat";
|
|
||||||
LOG.trace("loadFromAsset() assetName={}", assetName);
|
|
||||||
|
|
||||||
try (InputStream is = context.getAssets().open(assetName);
|
|
||||||
BufferedInputStream stream = new BufferedInputStream(is)) {
|
|
||||||
|
|
||||||
loadFromStream(stream);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("loadFromAsset()", e);
|
|
||||||
}
|
|
||||||
LOG.trace("loadFromAsset() finished");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadFromFile(String fileName) {
|
|
||||||
LOG.debug("loadFromFile({}) started", fileName);
|
|
||||||
|
|
||||||
try (FileInputStream fis = new FileInputStream(fileName);
|
|
||||||
BufferedInputStream stream = new BufferedInputStream(fis)) {
|
|
||||||
|
|
||||||
loadFromStream(stream);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("loadFromFile()", e);
|
|
||||||
}
|
|
||||||
LOG.trace("loadFromFile() finished");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "AbstractDatabaseDataSlice{" +
|
return "AbstractDatabaseDataSlice{" +
|
||||||
|
@ -11,7 +11,6 @@ import java.io.BufferedInputStream;
|
|||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileFilter;
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -24,11 +23,16 @@ import java.util.List;
|
|||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
import dummydomain.yetanothercallblocker.sia.network.WebService;
|
import dummydomain.yetanothercallblocker.sia.network.WebService;
|
||||||
|
import dummydomain.yetanothercallblocker.sia.utils.FileUtils;
|
||||||
import dummydomain.yetanothercallblocker.sia.utils.Utils;
|
import dummydomain.yetanothercallblocker.sia.utils.Utils;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
import static dummydomain.yetanothercallblocker.sia.utils.FileUtils.getDataDir;
|
||||||
|
import static dummydomain.yetanothercallblocker.sia.utils.FileUtils.getDataDirPath;
|
||||||
|
import static dummydomain.yetanothercallblocker.sia.utils.FileUtils.openFile;
|
||||||
|
|
||||||
public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSlice, CommunityDatabaseItem> {
|
public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSlice, CommunityDatabaseItem> {
|
||||||
|
|
||||||
private enum UpdateResult {
|
private enum UpdateResult {
|
||||||
@ -37,13 +41,22 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CommunityDatabase.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CommunityDatabase.class);
|
||||||
|
|
||||||
private int siaAppVersion = 114;
|
private static final int FALLBACK_APP_VERSION = 114;
|
||||||
|
|
||||||
|
private int siaAppVersion = FALLBACK_APP_VERSION;
|
||||||
|
|
||||||
|
protected final String secondaryPathPrefix;
|
||||||
|
|
||||||
private SparseArray<CommunityDatabaseDataSlice> secondarySliceCache = new SparseArray<>();
|
private SparseArray<CommunityDatabaseDataSlice> secondarySliceCache = new SparseArray<>();
|
||||||
|
|
||||||
@SuppressLint("UseSparseArrays") // uses null as a special value
|
@SuppressLint("UseSparseArrays") // uses null as a special value
|
||||||
private SparseArray<Boolean> existingSecondarySliceFiles = new SparseArray<>();
|
private SparseArray<Boolean> existingSecondarySliceFiles = new SparseArray<>();
|
||||||
|
|
||||||
|
public CommunityDatabase(String pathPrefix, String secondaryPathPrefix) {
|
||||||
|
super(pathPrefix);
|
||||||
|
this.secondaryPathPrefix = secondaryPathPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
public int getEffectiveDbVersion() {
|
public int getEffectiveDbVersion() {
|
||||||
checkLoaded();
|
checkLoaded();
|
||||||
|
|
||||||
@ -56,18 +69,27 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getAssetNamePrefix() {
|
protected String getNamePrefix() {
|
||||||
return "data_slice_";
|
return "data_slice_";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean load() {
|
protected void reset() {
|
||||||
if (!super.load()) return false;
|
super.reset();
|
||||||
|
|
||||||
LOG.debug("load() started");
|
siaAppVersion = FALLBACK_APP_VERSION;
|
||||||
|
secondarySliceCache.clear();
|
||||||
|
existingSecondarySliceFiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
String fileName = getAssetPathPrefix() + "sia_info.dat";
|
@Override
|
||||||
try (InputStream is = Utils.getContext().getAssets().open(fileName);
|
protected boolean load(boolean useAssets) {
|
||||||
|
if (!super.load(useAssets)) return false;
|
||||||
|
|
||||||
|
LOG.debug("load() started; useAssets={}", useAssets);
|
||||||
|
|
||||||
|
String fileName = getPathPrefix() + "sia_info.dat";
|
||||||
|
try (InputStream is = openFile(fileName, useAssets);
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
|
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is))) {
|
||||||
|
|
||||||
if (!"SIA".equals(bufferedReader.readLine())) {
|
if (!"SIA".equals(bufferedReader.readLine())) {
|
||||||
@ -87,7 +109,7 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void loadInfoDataAfterLoadedHook() {
|
protected void loadInfoDataAfterLoadedHook(boolean useAssets) {
|
||||||
int oldDbVersion = Utils.getSettings().getBaseDbVersion();
|
int oldDbVersion = Utils.getSettings().getBaseDbVersion();
|
||||||
if (baseDatabaseVersion != oldDbVersion) {
|
if (baseDatabaseVersion != oldDbVersion) {
|
||||||
LOG.info("loadInfoDataAfterLoadedHook() base version changed; resetting secondary DB;" +
|
LOG.info("loadInfoDataAfterLoadedHook() base version changed; resetting secondary DB;" +
|
||||||
@ -145,7 +167,7 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
String path = getCachedSecondarySliceFilePath(sliceId);
|
String path = getCachedSecondarySliceFilePath(sliceId);
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
LOG.trace("getSecondaryDataSlice() slice file exists, loading from: {}", path);
|
LOG.trace("getSecondaryDataSlice() slice file exists, loading from: {}", path);
|
||||||
communityDatabaseDataSlice.loadFromFile(path);
|
loadSlice(communityDatabaseDataSlice, path, false);
|
||||||
} else {
|
} else {
|
||||||
LOG.trace("getSecondaryDataSlice() slice file doesn't exist");
|
LOG.trace("getSecondaryDataSlice() slice file doesn't exist");
|
||||||
}
|
}
|
||||||
@ -160,30 +182,25 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
String path = getSecondarySliceFilePath(id);
|
String path = getSecondarySliceFilePath(id);
|
||||||
Boolean exists = existingSecondarySliceFiles.get(id, null);
|
Boolean exists = existingSecondarySliceFiles.get(id, null);
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
exists = new File(path).exists();
|
exists = new File(getDataDirPath() + path).exists();
|
||||||
existingSecondarySliceFiles.put(id, exists);
|
existingSecondarySliceFiles.put(id, exists);
|
||||||
}
|
}
|
||||||
return exists ? path : null;
|
return exists ? path : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSecondarySliceFilePath(int id) {
|
private String getSecondarySliceFilePath(int id) {
|
||||||
File dir = Utils.getContext().getFilesDir();
|
return getSecondaryDbPathPrefix() + id + ".sia";
|
||||||
return dir.getAbsolutePath() + "/sia_updates_" + id + ".sia";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetSecondaryDatabase() {
|
public void resetSecondaryDatabase() {
|
||||||
LOG.debug("resetSecondaryDatabase() started");
|
LOG.debug("resetSecondaryDatabase() started");
|
||||||
|
|
||||||
FileFilter fileFilter = new FileFilter() {
|
File dir = new File(getDataDir(), getSecondaryDbPathPrefix());
|
||||||
@Override
|
if (dir.exists()) {
|
||||||
public boolean accept(File pathname) {
|
for (File file : dir.listFiles()) {
|
||||||
return pathname.getName().endsWith(".sia");
|
if (!file.delete()) {
|
||||||
}
|
LOG.warn("resetSecondaryDatabase() failed to delete secondary DB file: {}", file.getAbsolutePath());
|
||||||
};
|
}
|
||||||
|
|
||||||
for (File file : Utils.getContext().getFilesDir().listFiles(fileFilter)) {
|
|
||||||
if (!file.delete()) {
|
|
||||||
LOG.warn("resetSecondaryDatabase() failed to delete secondary DB file: {}", file.getAbsolutePath());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,9 +211,22 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
LOG.info("resetSecondaryDatabase() secondary DB was reset");
|
LOG.info("resetSecondaryDatabase() secondary DB was reset");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getSecondaryDbPathPrefix() {
|
||||||
|
return secondaryPathPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createSecondaryDbDirectory() {
|
||||||
|
FileUtils.createDirectory(getDataDir(), getSecondaryDbPathPrefix());
|
||||||
|
}
|
||||||
|
|
||||||
public void updateSecondaryDb() {
|
public void updateSecondaryDb() {
|
||||||
LOG.info("updateSecondaryDb() started");
|
LOG.info("updateSecondaryDb() started");
|
||||||
|
|
||||||
|
if (!isOperational()) {
|
||||||
|
LOG.warn("updateSecondaryDb() DB is not operational, update aborted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
long startTimestamp = System.currentTimeMillis();
|
long startTimestamp = System.currentTimeMillis();
|
||||||
|
|
||||||
boolean updated = false;
|
boolean updated = false;
|
||||||
@ -267,6 +297,7 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
LOG.trace("updateSecondaryDbInternal() loading slice from received data");
|
LOG.trace("updateSecondaryDbInternal() loading slice from received data");
|
||||||
CommunityDatabaseDataSlice slice = new CommunityDatabaseDataSlice();
|
CommunityDatabaseDataSlice slice = new CommunityDatabaseDataSlice();
|
||||||
if (slice.loadFromStream(bis)) {
|
if (slice.loadFromStream(bis)) {
|
||||||
|
createSecondaryDbDirectory();
|
||||||
LOG.trace("updateSecondaryDbInternal() distributing slice");
|
LOG.trace("updateSecondaryDbInternal() distributing slice");
|
||||||
updateSecondaryWithSlice(slice);
|
updateSecondaryWithSlice(slice);
|
||||||
}
|
}
|
||||||
@ -331,11 +362,11 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
if (newSlice.partialClone(dataSlice, sliceId, shortSliceIdToIndexMap, shortSliceIdToIndexToDeleteMap)) {
|
if (newSlice.partialClone(dataSlice, sliceId, shortSliceIdToIndexMap, shortSliceIdToIndexToDeleteMap)) {
|
||||||
CommunityDatabaseDataSlice sliceFromExistingFile = new CommunityDatabaseDataSlice();
|
CommunityDatabaseDataSlice sliceFromExistingFile = new CommunityDatabaseDataSlice();
|
||||||
if (getCachedSecondarySliceFilePath(sliceId) != null) {
|
if (getCachedSecondarySliceFilePath(sliceId) != null) {
|
||||||
sliceFromExistingFile.loadFromFile(filePath);
|
loadSlice(sliceFromExistingFile, filePath, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (BufferedOutputStream stream = new BufferedOutputStream(
|
try (BufferedOutputStream stream = new BufferedOutputStream(
|
||||||
new FileOutputStream(filePath + ".update", false))) {
|
new FileOutputStream(getDataDirPath() + filePath + ".update", false))) {
|
||||||
sliceFromExistingFile.writeMerged(newSlice, stream);
|
sliceFromExistingFile.writeMerged(newSlice, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +379,7 @@ public class CommunityDatabase extends AbstractDatabase<CommunityDatabaseDataSli
|
|||||||
LOG.debug("updateSecondaryWithSlice() update files created, renaming files");
|
LOG.debug("updateSecondaryWithSlice() update files created, renaming files");
|
||||||
|
|
||||||
for (int sliceId : updatedIndexes) {
|
for (int sliceId : updatedIndexes) {
|
||||||
String filePath = getSecondarySliceFilePath(sliceId);
|
String filePath = getDataDirPath() + getSecondarySliceFilePath(sliceId);
|
||||||
|
|
||||||
File updatedFile = new File(filePath + ".update");
|
File updatedFile = new File(filePath + ".update");
|
||||||
File oldFile = new File(filePath);
|
File oldFile = new File(filePath);
|
||||||
|
@ -141,11 +141,6 @@ public class CommunityDatabaseDataSlice extends AbstractDatabaseDataSlice<Commun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getAssetNamePrefix() {
|
|
||||||
return "data_slice_";
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean writeMerged(CommunityDatabaseDataSlice newSlice, BufferedOutputStream outputStream) {
|
public boolean writeMerged(CommunityDatabaseDataSlice newSlice, BufferedOutputStream outputStream) {
|
||||||
LOG.debug("writeMerged() started with newSlice={}", newSlice);
|
LOG.debug("writeMerged() started with newSlice={}", newSlice);
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package dummydomain.yetanothercallblocker.sia.model.database;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import dummydomain.yetanothercallblocker.sia.SiaConstants;
|
||||||
|
import dummydomain.yetanothercallblocker.sia.network.DbDownloader;
|
||||||
|
import dummydomain.yetanothercallblocker.sia.utils.FileUtils;
|
||||||
|
|
||||||
|
public class DbManager {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DbManager.class);
|
||||||
|
|
||||||
|
public static boolean downloadMainDb() {
|
||||||
|
String url = "https://gitlab.com/xynngh/YetAnotherCallBlocker_data/raw/zip_v1/archives/sia.zip";
|
||||||
|
return downloadMainDb(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean downloadMainDb(String url) {
|
||||||
|
LOG.debug("downloadMainDb() started");
|
||||||
|
|
||||||
|
File dataDir = FileUtils.getDataDir();
|
||||||
|
|
||||||
|
String siaDir = SiaConstants.SIA_PATH_PREFIX;
|
||||||
|
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, FileUtils.getDataDirPath() + tmpUpdateDir)) {
|
||||||
|
LOG.debug("downloadMainDb() downloaded and unpacked");
|
||||||
|
new File(dataDir, siaDir).renameTo(new File(dataDir, oldDir));
|
||||||
|
new File(dataDir, tmpUpdateDir).renameTo(new File(dataDir, siaDir));
|
||||||
|
FileUtils.delete(dataDir, oldDir);
|
||||||
|
LOG.debug("downloadMainDb() folders moved");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOG.warn("downloadMainDb() failed downloading");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,8 +2,12 @@ package dummydomain.yetanothercallblocker.sia.model.database;
|
|||||||
|
|
||||||
public class FeaturedDatabase extends AbstractDatabase<FeaturedDatabaseDataSlice, FeaturedDatabaseItem> {
|
public class FeaturedDatabase extends AbstractDatabase<FeaturedDatabaseDataSlice, FeaturedDatabaseItem> {
|
||||||
|
|
||||||
|
public FeaturedDatabase(String pathPrefix) {
|
||||||
|
super(pathPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getAssetNamePrefix() {
|
protected String getNamePrefix() {
|
||||||
return "featured_slice_";
|
return "featured_slice_";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,9 +31,4 @@ public class FeaturedDatabaseDataSlice extends AbstractDatabaseDataSlice<Feature
|
|||||||
names[index] = stream.readUtf8StringChars(nameLength);
|
names[index] = stream.readUtf8StringChars(nameLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getAssetNamePrefix() {
|
|
||||||
return "featured_slice_";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package dummydomain.yetanothercallblocker.sia.utils;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class FileUtils {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class);
|
||||||
|
|
||||||
|
public static File getDataDir() {
|
||||||
|
return Utils.getContext().getFilesDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDataDirPath() {
|
||||||
|
return getDataDir().getAbsolutePath() + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InputStream openFile(String fileName, boolean asset) throws IOException {
|
||||||
|
if (asset) {
|
||||||
|
return Utils.getContext().getAssets().open(fileName);
|
||||||
|
} else {
|
||||||
|
return new FileInputStream(getDataDirPath() + fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File createDirectory(String path) {
|
||||||
|
return createDirectory(new File(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -25,6 +25,54 @@
|
|||||||
android:paddingRight="@dimen/item_padding"
|
android:paddingRight="@dimen/item_padding"
|
||||||
android:showDividers="middle">
|
android:showDividers="middle">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/noDbText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_marginStart="@dimen/text_margin"
|
||||||
|
android:layout_marginLeft="@dimen/text_margin"
|
||||||
|
android:paddingTop="@dimen/item_padding"
|
||||||
|
android:text="@string/no_main_db"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:textColor="@color/rateNegative"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/downloadDbButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/text_margin"
|
||||||
|
android:layout_marginBottom="@dimen/text_margin"
|
||||||
|
android:onClick="onDownloadDbClick"
|
||||||
|
android:text="@string/download_main_db"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/downloadingDbText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_marginStart="@dimen/text_margin"
|
||||||
|
android:layout_marginLeft="@dimen/text_margin"
|
||||||
|
android:paddingTop="@dimen/item_padding"
|
||||||
|
android:text="@string/downloading_db"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dbCouldNotBeDownloaded"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_marginStart="@dimen/text_margin"
|
||||||
|
android:layout_marginLeft="@dimen/text_margin"
|
||||||
|
android:paddingTop="@dimen/item_padding"
|
||||||
|
android:text="@string/db_could_not_be_downloaded"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:textColor="@color/rateNegative"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.SwitchCompat
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
android:id="@+id/notificationsEnabledSwitch"
|
android:id="@+id/notificationsEnabledSwitch"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -47,10 +47,14 @@
|
|||||||
<string name="reviews_loading">Loading reviews…</string>
|
<string name="reviews_loading">Loading reviews…</string>
|
||||||
|
|
||||||
<string name="general_settings">General settings</string>
|
<string name="general_settings">General settings</string>
|
||||||
|
<string name="no_main_db">Database is not present! For the app to function offline you need to download DB. Press the button below to do it.</string>
|
||||||
|
<string name="downloading_db">Downloading DB…</string>
|
||||||
|
<string name="db_could_not_be_downloaded">Database couldn\'t be downloaded</string>
|
||||||
|
|
||||||
<string name="incoming_call_notifications_enabled">Incoming call notifications enabled</string>
|
<string name="incoming_call_notifications_enabled">Incoming call notifications enabled</string>
|
||||||
<string name="block_calls">Block unwanted calls</string>
|
<string name="block_calls">Block unwanted calls</string>
|
||||||
<string name="auto_update_enabled">Auto-update enabled</string>
|
<string name="auto_update_enabled">Auto-update enabled</string>
|
||||||
|
<string name="download_main_db">Download database</string>
|
||||||
|
|
||||||
<string name="open_debug_activity">Open debug screen</string>
|
<string name="open_debug_activity">Open debug screen</string>
|
||||||
<string name="debug_activity_label">Debug</string>
|
<string name="debug_activity_label">Debug</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user