app/src/main/java/ca/marshallasch/veil/database/Database.java
package ca.marshallasch.veil.database;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.preference.PreferenceManager;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.support.annotation.WorkerThread;
import android.support.v4.util.Pair;
import android.util.Log;
import org.mindrot.jbcrypt.BCrypt;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.concurrent.ThreadSafe;
import ca.marshallasch.veil.R;
import ca.marshallasch.veil.database.BlockContract.BlockEntry;
import ca.marshallasch.veil.database.KnownPostsContract.KnownPostsEntry;
import ca.marshallasch.veil.database.NotificationContract.NotificationEntry;
import ca.marshallasch.veil.database.PeerListContract.PeerListEntry;
import ca.marshallasch.veil.database.UserContract.UserEntry;
import ca.marshallasch.veil.database.SyncStatsContract.SyncStatsEntry;
import ca.marshallasch.veil.proto.DhtProto;
import ca.marshallasch.veil.utilities.Util;
/**
* This is the database helper class, all actions on the database Must go through this.
* This class is a singleton and must be accessed through {@link #getInstance(Context)}.
*
* Note the at SQLite does not impose length restrictions.
*
* Any call that will read from or write to the database will be synchronous so that one one can
* be executing at a time.
*
* @author Marshall Asch
* @version 1.0
* @since 2018-05-31
*/
@ThreadSafe
public class Database extends SQLiteOpenHelper
{
private static String DATABASE_NAME = "contentDiscoveryTables";
private static final int DATABASE_VERSION = 10;
// this is for the singleton
private static Database instance = null;
private static final AtomicInteger openCounter = new AtomicInteger();
private final Context context;
private Database(final Context context)
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.context = context;
}
/**
* This is used to initiate an instance of the class so only one will ever exist at a time.
*
* @param c the applications context
* @return the existing instance of the database, or creates one and returns that.
*/
public static synchronized Database getInstance(final Context c)
{
if (instance == null) {
instance = new Database(c.getApplicationContext());
}
openCounter.incrementAndGet();
return instance;
}
/**
* This is used to initiate an instance of the class so only one will ever exist at a time.
* This is used for integration tests so that if the tests are run on a real device it will
* use a separate database for testing.
*
* @param c the applications context
* @return the existing instance of the database, or creates one and returns that.
*/
static synchronized Database getInstance_TETSING(final Context c)
{
if (instance == null) {
DATABASE_NAME += "_TESTING";
instance = new Database(c.getApplicationContext());
}
openCounter.incrementAndGet();
return instance;
}
/**
* This will make sure that the database is only closed when it is actually finished.
*/
@Override
public void close()
{
if (openCounter.decrementAndGet() == 0) {
super.close();
}
}
/**
* This will clear all the data from the database.
*/
public void clear(){
getWritableDatabase().delete(BlockEntry.TABLE_NAME, null, null);
getWritableDatabase().delete(NotificationEntry.TABLE_NAME, null, null);
getWritableDatabase().delete(UserEntry.TABLE_NAME, null, null);
getWritableDatabase().delete(KnownPostsEntry.TABLE_NAME, null, null);
getWritableDatabase().delete(PeerListEntry.TABLE_NAME, null, null);
getWritableDatabase().delete(SyncStatsEntry.TABLE_NAME, null, null);
}
/**
* Create all of the tables in the database
*
* @param db the underlying database object
*/
@Override
public void onCreate(SQLiteDatabase db)
{
db.execSQL(BlockContract.SQL_CREATE_BLOCK_USERS);
db.execSQL(NotificationContract.SQL_CREATE_POST_NOTIFICATION);
db.execSQL(UserContract.SQL_CREATE_USERS);
db.execSQL(KnownPostsContract.SQL_CREATE_KNOWN_POSTS);
db.execSQL(PeerListContract.SQL_CREATE_PEER_LIST);
db.execSQL(SyncStatsContract.SQL_CREATE_SYNC_STATS);
}
/**
* Do the appropriate upgrade path for the database
* @param db the database to be upgraded
* @param oldVersion the old version of the DB
* @param newVersion the new version of the DB
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if(oldVersion < 3){
Migrations.upgradeV3(db);
}
if (oldVersion < 4) {
Migrations.upgradeV4(db);
}
if (oldVersion < 6) {
Migrations.upgradeV6(db);
}
if (oldVersion < 7) {
Migrations.upgradeV7(db);
}
if (oldVersion < 8) {
Migrations.upgradeV8(db);
}
if (oldVersion < 9) {
Migrations.upgradeV9(db);
}
if (oldVersion < 10){
Migrations.upgradeV10(db);
}
}
/**
* Will add a row to the block user table. If a user is already in the table then it will
* overwrite the previous entry.
* <p>
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*
* @param userID the ID of the user to block, max
* @param userHash this is the hash of the user for the DHT
* @return true if the user was successfully added, false if something went wrong.
*/
@WorkerThread
public boolean blockUser(@Size(max = 36) String userID, @Size(max = 36) String userHash)
{
// make sure params are not null
if (userHash == null || userID == null) {
return false;
}
ContentValues values = new ContentValues();
values.put(BlockEntry.COLUMN_USER_ID, userID);
values.put(BlockEntry.COLUMN_USER_HASH, userHash);
values.put(BlockEntry.COLUMN_TIMESTAMP, System.currentTimeMillis());
// note this is a potentially long running operation.
long id;
synchronized (this) {
id = getWritableDatabase().insertWithOnConflict(BlockEntry.TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
return id != -1;
}
/**
* Will remove a row from the block user table. If the user was not blocked it will
* return false.
* <p>
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*
* @param userID the ID of the user to unblock
* @return true if 1 user was unblocked otherwise false
*/
@WorkerThread
public boolean unblockUser(@Size(max = 36) String userID)
{
if (userID == null) {
return false;
}
// Define 'where' part of query.
String selection = BlockEntry.COLUMN_USER_ID + " = ?";
// Specify arguments in placeholder order.
String[] selectionArgs = {userID};
int numDeleted;
synchronized (this) {
numDeleted = getWritableDatabase().delete(BlockEntry.TABLE_NAME, selection, selectionArgs);
}
// makes sure only 1 row was removed, anything else would be an error
return numDeleted == 1;
}
/**
* This function will check if a user is blocked
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
*
* @param userID the ID of the user to check
* @return true if the user is blocked false otherwise
*/
@WorkerThread
public boolean checkBlocked(@Size(max = 36) String userID)
{
if (userID == null) {
return false;
}
// Filter results WHERE userID = 'id'
String where = BlockEntry.COLUMN_USER_ID + " = ?";
String[] whereArgs = {userID};
int count = getCount(BlockEntry.TABLE_NAME, where, whereArgs);
return count == 1;
}
// TODO: 2018-05-31 Create a function that will get the list of all blocked user objects
/**
* Register a to receive notifications of new posts from a user or for comments about a specific
* post.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param hash the hash of the post or user in the DHT
* @return true if it was successfully registered, false otherwise
*/
@WorkerThread
public boolean registerForNotification(@Size(max = 36) String hash) {
if (hash == null) {
return false;
}
ContentValues values = new ContentValues();
values.put(NotificationEntry.COLUMN_HASH, hash);
values.put(NotificationEntry.COLUMN_TIMESTAMP, System.currentTimeMillis());
// note this is a potentially long running operation.
long id;
synchronized (this) {
id = getWritableDatabase().insertWithOnConflict(NotificationEntry.TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
return id != -1;
}
/**
* Unregister a to receive notifications of new posts from a user or for comments about a
* specific post.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param hash the hash of the post or user in the DHT
* @return true if it was successfully unregistered, false otherwise
*/
@WorkerThread
public boolean unregisterForNotification(@Size(max = 36) String hash) {
if (hash == null) {
return false;
}
// Define 'where' part of query.
String selection = NotificationEntry.COLUMN_HASH + " = ?";
// Specify arguments in placeholder order.
String[] selectionArgs = {hash};
int numDeleted;
synchronized (this) {
numDeleted = getWritableDatabase().delete(NotificationEntry.TABLE_NAME, selection, selectionArgs);
}
// makes sure only 1 row was removed, anything else would be an error
return numDeleted == 1;
}
/**
* Check if it should give a notification for the new post or comment, allows you to specify
* that you want to receive notifications for posts by specific users, or comments on a specific
* post.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param hash the hash that is used to identify the user or post in the DHT
* @return true if a notification should be given otherwise false
*/
@WorkerThread
public boolean checkGiveNotification(@Size(max = 36) String hash) {
if (hash == null) {
return false;
}
// check if notifications have been temporarily muted
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
int status = preferences.getInt(context.getString(R.string.pref_notifications), 0);
if (status == 1) {
return true;
} else if (status == 2) {
return false;
}
// Filter results WHERE userHash = 'hash'
String where = NotificationEntry.COLUMN_HASH + " = ?";
String[] whereArgs = {hash};
int count = getCount(NotificationEntry.TABLE_NAME, where, whereArgs);
return count == 1;
}
/**
* This will add a entry to the table to make it easier for look up later.
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*
* @param postHash The SHA256 hash of the post object
* @param commentHash the SHA256 hash of the comment object
* @return <code>true</code> if it is successfully inserted, <code>false</code> otherwise
*/
@WorkerThread
public boolean insertKnownPost(@Size(max = 36) String postHash, @Nullable @Size(max = 36) String commentHash) {
// the post hash has to be set but the comment hash can be null, if the post hash is empty return false too.
if (postHash == null || postHash.isEmpty()) {
return false;
}
commentHash = commentHash == null? "" : commentHash;
ContentValues values = new ContentValues();
values.put(KnownPostsEntry.COLUMN_POST_HASH, postHash);
values.put(KnownPostsEntry.COLUMN_COMMENT_HASH, commentHash);
values.put(KnownPostsEntry.COLUMN_TIME_INSERTED, new Date().getTime());
// note this is a potentially long running operation.
long id;
synchronized (this) {
id = getWritableDatabase().insert(KnownPostsEntry.TABLE_NAME, null, values);
}
Log.d("INSERT", "ID: " + id + " POST: " + postHash);
return id != -1;
}
/**
* This will create a new user account and will assign them a new UUID that is
* <a href="https://tools.ietf.org/html/rfc4122">RFC 4122</a> type 4 compliant.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread.
*
* @see <a href="https://tools.ietf.org/html/rfc4122">RFC 4122</a>
*
* @param firstName the users first name
* @param lastName the users last name
* @param email the users email address (also their username
* @param password the plain text password of the user
* @return null on failure and the {@link DhtProto.User} object on success
*/
@WorkerThread
public DhtProto.User createUser(String firstName, String lastName, String email, String password) {
Date createdAt = new Date();
UUID uuid = UUID.randomUUID();
String passHash = BCrypt.hashpw(password, BCrypt.gensalt(10));
com.google.protobuf.Timestamp time = Util.millisToTimestamp(createdAt.getTime());
// this is the object that would get sent to the DHT
DhtProto.User user = DhtProto.User.newBuilder()
.setEmail(email)
.setFirstName(firstName)
.setLastName(lastName)
.setUuid(uuid.toString())
.setTimestamp(time)
.build();
ContentValues values = new ContentValues();
values.put(UserEntry.COLUMN_EMAIL_ADDRESS, email);
values.put(UserEntry.COLUMN_ID, uuid.toString());
values.put(UserEntry.COLUMN_FIRST_NAME, firstName);
values.put(UserEntry.COLUMN_LAST_NAME, lastName);
values.put(UserEntry.COLUMN_PASSWORD, passHash);
values.put(UserEntry.COLUMN_TIMESTAMP, createdAt.getTime());
// note this is a potentially long running operation.
long id;
synchronized (this) {
id = getWritableDatabase().insert(UserEntry.TABLE_NAME, null, values);
}
// check if the creation was successful, return the user if it was
return id != -1 ? user : null;
}
/**
* This function will check if a user is able to login to this device. Note that if the email
* address is associated with multiple accounts then the login will authenticate them on the
* first account that it finds with a matching username + password combination.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param email the users email address for the account
* @param password the users plain text password for the account
* @return {@link DhtProto.User} object that contains all the account information for the user.
*/
@Nullable
@WorkerThread
public DhtProto.User login(@Nullable String email,@Nullable String password) {
if (email == null || password == null) {
return null;
}
String[] projection = {
UserEntry.COLUMN_ID,
UserEntry.COLUMN_EMAIL_ADDRESS,
UserEntry.COLUMN_FIRST_NAME,
UserEntry.COLUMN_LAST_NAME,
UserEntry.COLUMN_PASSWORD,
UserEntry.COLUMN_TIMESTAMP
};
// Filter results WHERE "email address" = 'My email'
String selection = UserEntry.COLUMN_EMAIL_ADDRESS + " = ?";
String[] selectionArgs = { email };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
UserEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort the rows
);
}
DhtProto.User user = null;
// check each of the accounts that have the same email address.
while(cursor.moveToNext()) {
String userID = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_ID));
String passwordHash = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_PASSWORD));
String firstName = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_FIRST_NAME));
String lastName = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_LAST_NAME));
long millis = cursor.getLong(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_TIMESTAMP));
com.google.protobuf.Timestamp time = Util.millisToTimestamp(millis);
// check if the passwords match, only until first match is found
if (BCrypt.checkpw(password, passwordHash)) {
user = DhtProto.User.newBuilder()
.setEmail(email)
.setFirstName(firstName)
.setLastName(lastName)
.setUuid(userID)
.setTimestamp(time)
.build();
break;
}
}
cursor.close();
return user;
}
/**
* This function is called when a userID was sent in the intent to start the activity.
* This will login the user without the username and password and is only called when the app
* restarts itself.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param uuid the id of the user to login
* @return {@link DhtProto.User} object that contains all the account information for the user.
*/
@Nullable
@WorkerThread
public DhtProto.User getUser(@Nullable String uuid) {
if (uuid == null) {
return null;
}
String[] projection = {
UserEntry.COLUMN_ID,
UserEntry.COLUMN_EMAIL_ADDRESS,
UserEntry.COLUMN_FIRST_NAME,
UserEntry.COLUMN_LAST_NAME,
UserEntry.COLUMN_TIMESTAMP
};
// Filter results WHERE "UUID" = 'My uuid'
String selection = UserEntry.COLUMN_ID + " = ?";
String[] selectionArgs = { uuid };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
UserEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort the rows
);
}
DhtProto.User user = null;
// check each of the accounts that have the same email address.
while(cursor.moveToNext()) {
String userID = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_ID));
String email = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_EMAIL_ADDRESS));
String firstName = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_FIRST_NAME));
String lastName = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_LAST_NAME));
long millis = cursor.getLong(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_TIMESTAMP));
com.google.protobuf.Timestamp time = Util.millisToTimestamp(millis);
user = DhtProto.User.newBuilder()
.setEmail(email)
.setFirstName(firstName)
.setLastName(lastName)
.setUuid(userID)
.setTimestamp(time)
.build();
}
cursor.close();
return user;
}
/**
* This function will find the user with the username and password then update the password.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param email the email address for the account to update
* @param oldPassword the current password for the account to update
* @param newPassword the new password
* @return true if it updates successfully, false if something unexpected happens
*/
@WorkerThread
public boolean updatePassword(@Nullable String email, @Nullable String oldPassword, @Nullable String newPassword) {
if (email == null || oldPassword == null || newPassword == null) {
return false;
}
// hash the new password
String passHash = BCrypt.hashpw(newPassword, BCrypt.gensalt(10));
int numUpdated = 0;
// going to check if the old password finds a match
String[] projection = {
UserEntry._ID,
UserEntry.COLUMN_PASSWORD
};
// Filter results WHERE "email address" = 'My email'
String selection = UserEntry.COLUMN_EMAIL_ADDRESS + " = ?";
String[] selectionArgs = { email };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
UserEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort the rows
);
}
// check each of the accounts that have the same email address.
while(cursor.moveToNext()) {
String passwordHash = cursor.getString(cursor.getColumnIndexOrThrow(UserEntry.COLUMN_PASSWORD));
int userRowID = cursor.getInt(cursor.getColumnIndexOrThrow(UserEntry._ID));
// check if the passwords match, only until first match has its password changed
if (BCrypt.checkpw(oldPassword, passwordHash)) {
selection = UserEntry._ID + " = ?";
selectionArgs = new String[]{ String.valueOf(userRowID)};
ContentValues values = new ContentValues();
values.put(UserEntry.COLUMN_PASSWORD, passHash);
synchronized (this) {
numUpdated = getWritableDatabase().update(
UserEntry.TABLE_NAME,
values,
selection,
selectionArgs
);
}
break;
}
}
cursor.close();
// check if only 1 row got updated
return numUpdated == 1;
}
/**
* This function will update the email address associated with an account.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param uuid the unique ID for the user
* @param newEmail the new email address for the user
* @return true if it was updated successfully
*/
@WorkerThread
public boolean updateEmail(@Nullable String uuid, @Nullable String newEmail) {
if (uuid == null || newEmail == null) {
return false;
}
// Filter results WHERE "userID" = 'My uuid'
String selection = UserEntry.COLUMN_ID + " = ?";
String[] selectionArgs = { uuid };
ContentValues values = new ContentValues();
values.put(UserEntry.COLUMN_EMAIL_ADDRESS, newEmail);
int numUpdated;
synchronized (this) {
numUpdated = getWritableDatabase().update(
UserEntry.TABLE_NAME,
values,
selection,
selectionArgs
);
}
// check if only 1 row got updated
return numUpdated == 1;
}
/**
* This function will update the name of the user in the database for the account.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param uuid the unique ID for the user
* @param firstName the new first name
* @param lastName the new last name
* @return true if it is successful otherwise false
*/
@WorkerThread
public boolean updateName(@Nullable String uuid, @Nullable String firstName, @Nullable String lastName) {
if (uuid == null || firstName == null || lastName == null) {
return false;
}
// Filter results WHERE "userID" = 'My uuid'
String selection = UserEntry.COLUMN_ID + " = ?";
String[] selectionArgs = { uuid };
ContentValues values = new ContentValues();
values.put(UserEntry.COLUMN_FIRST_NAME, firstName);
values.put(UserEntry.COLUMN_LAST_NAME, lastName);
int numUpdated;
synchronized (this) {
numUpdated = getWritableDatabase().update(
UserEntry.TABLE_NAME,
values,
selection,
selectionArgs
);
}
// check if only 1 row got updated
return numUpdated == 1;
}
// TODO: 2018-06-05 Add functions to export the user profile
// TODO: 2018-06-05 Add function to import a user profile
// TODO: 2018-06-05 Add A function to update the account information
/**
* Gets a list of all the hashes of the posts that are stored.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @return an {@link ArrayList} with the list of post hashes
*/
@WorkerThread
public List<String> getPostHashes() {
String[] projection = {
KnownPostsEntry.COLUMN_POST_HASH
};
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
true,
KnownPostsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null, // don't sort
null // no limit to the results
);
}
List<String> hashes = new ArrayList<>();
// get each post hash that is in the list
while(cursor.moveToNext()) {
String hash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_POST_HASH));
// add the hash to the list
hashes.add(hash);
}
cursor.close();
return hashes;
}
/**
* This function will get the time stamp of the last time that data was sent to a peer.
* If the <code>peerID</code> is null or there are no entries in the database then the timestamp
* of the unix epoch is given (midnight January 1st 1970).
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param peerID the {@link io.left.rightmesh.id.MeshId} of the peer in string form.
* @return a {@link Date} object representing the timestamp.
*/
@WorkerThread
public Date getTimeLastSentData(String peerID) {
Date time = new Date(0);
// stop ig the the peerID is missing
if (peerID == null) {
return time;
}
String[] projection = {
PeerListEntry.COLUMN_TIME_LAST_SENT
};
String selection = PeerListEntry.COLUMN_PEER_MESH_ID + " = ?";
String[] selectionArgs = { peerID };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
true,
PeerListEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null, // don't sort
null // no limit to the results
);
}
while(cursor.moveToNext()) {
long millis = cursor.getLong(cursor.getColumnIndexOrThrow(PeerListEntry.COLUMN_TIME_LAST_SENT));
time = new Date(millis);
}
cursor.close();
return time;
}
/**
* This function will update the time that data was last sent to a specific peer.
* If the peer has not been seen before then a new entry is inserted into the table.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param peerID the {@link io.left.rightmesh.id.MeshId} of the peer in string form.
* @return true if it was inserted successfully, false otherwise
*/
@WorkerThread
public boolean updateTimeLastSentData(String peerID) {
// stop if the the peerID is missing
if (peerID == null) {
return false;
}
String selection = PeerListEntry.COLUMN_PEER_MESH_ID + " = ?";
String[] selectionArgs = { peerID };
ContentValues values = new ContentValues();
values.put(PeerListEntry.COLUMN_PEER_MESH_ID, peerID);
values.put(PeerListEntry.COLUMN_TIME_LAST_SENT, new Date().getTime());
int numUpdated;
synchronized (this) {
numUpdated = getWritableDatabase().update(
PeerListEntry.TABLE_NAME, // The table to update
values, // The values to update
selection, // The columns for the WHERE clause
selectionArgs // The values for the WHERE clause
);
}
if (numUpdated != 1) {
// note this is a potentially long running operation.
long id;
synchronized (this) {
id = getWritableDatabase().insert(PeerListEntry.TABLE_NAME, null, values);
}
return id != -1;
}
return true;
}
/**
* Gets a list of all the hashes of the comments that belong to a specific post.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param postHash the hash of the post.
* @return an {@link ArrayList} with the list of comment hashes
*/
@WorkerThread
public List<String> getCommentHashes(String postHash) {
String[] projection = {
KnownPostsEntry.COLUMN_COMMENT_HASH
};
// Filter results WHERE userID = 'id'
String where = KnownPostsEntry.COLUMN_POST_HASH + " = ? AND " + KnownPostsEntry.COLUMN_COMMENT_HASH + " NOT NULL" ;
String[] whereArgs = {postHash};
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
true,
KnownPostsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
where, // The columns for the WHERE clause
whereArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null, // don't sort
null // no limit to the results
);
}
List<String> hashes = new ArrayList<>();
// get each post hash that is in the list
while(cursor.moveToNext()) {
String hash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_COMMENT_HASH));
// add the hash to the list
hashes.add(hash);
}
cursor.close();
return hashes;
}
/**
* This will get a list of the mappings between posts and comments.
* All posts will have 1 record where the comment hash is the empty string.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @return a list of pairs where pair.first is the post hash and pair.second is the comment
*/
@WorkerThread
@NonNull
public List<Pair<String, String>> dumpKnownPosts() {
String[] projection = {
KnownPostsEntry.COLUMN_POST_HASH,
KnownPostsEntry.COLUMN_COMMENT_HASH
};
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
KnownPostsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
List<Pair<String, String>> hashes = new ArrayList<>();
String postHash;
String commentHash;
// get each post hash that is in the list
while(cursor.moveToNext()) {
postHash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_POST_HASH));
commentHash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_COMMENT_HASH));
// add the hash to the list
hashes.add(new Pair<>(postHash, commentHash));
}
cursor.close();
return hashes;
}
/**
* This will get a list of the mappings between posts and comments that have been inserted
* after <code>since</code>.
* All posts will have 1 record where the comment hash is the empty string.
* If <code>since</code> is null then it will be set to the unix epoch.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param since the date that all the entries should be gotten since.
* @return a list of pairs where pair.first is the post hash and pair.second is the comment
*/
@WorkerThread
@NonNull
public List<Pair<String, String>> dumpKnownPosts(@Nullable Date since) {
List<Pair<String, String>> hashes = new ArrayList<>();
if (since == null) {
since = new Date(0);
}
String[] projection = {
KnownPostsEntry.COLUMN_POST_HASH,
KnownPostsEntry.COLUMN_COMMENT_HASH
};
String selection = KnownPostsEntry.COLUMN_TIME_INSERTED + " >= ?";
String[] selectionArgs = { String.valueOf(since.getTime()) };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
KnownPostsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
String postHash;
String commentHash;
// get each post hash that is in the list
while(cursor.moveToNext()) {
postHash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_POST_HASH));
commentHash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_COMMENT_HASH));
// add the hash to the list
hashes.add(new Pair<>(postHash, commentHash));
}
cursor.close();
return hashes;
}
/**
* This will check if a post has been marked as being read.
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param postHash the post to check
* @return true if the read flag has been set, otherwise false
*/
@WorkerThread
public boolean isRead(String postHash) {
String[] projection = { KnownPostsEntry.COLUMN_READ };
String selection = KnownPostsEntry.COLUMN_POST_HASH + " = ? AND " +
KnownPostsEntry.COLUMN_COMMENT_HASH + " == \"\" ";
String[] selectionArgs = { postHash };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
KnownPostsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
int read = 0;
// get each post hash that is in the list
while(cursor.moveToNext()) {
read = cursor.getInt(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_READ));
}
cursor.close();
return read == 1;
}
/**
* This will mark the post as being read or unread
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param postHash the post to check
* @param read if the post is being marked as read or unread
* @return true if it was successful false otherwise
*/
@WorkerThread
public boolean markRead(String postHash, boolean read) {
String selection = KnownPostsEntry.COLUMN_POST_HASH + " = ? AND " +
KnownPostsEntry.COLUMN_COMMENT_HASH + " == \"\" ";
String[] selectionArgs = { postHash };
ContentValues values = new ContentValues();
values.put(KnownPostsEntry.COLUMN_READ, read ? 1 : 0);
int numUpdated;
synchronized (this) {
numUpdated = getWritableDatabase().update(
KnownPostsEntry.TABLE_NAME,
values,
selection,
selectionArgs
);
}
return numUpdated == 1;
}
/**
* Count the number of matching rows in the table
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param tableName The name of the table to get the count for
* @param where the conditions that it is counting
* @param whereArgs the args
* @return the number of matching rows
*/
@WorkerThread
public int getCount(@NonNull String tableName, @Nullable String where, @Nullable String[] whereArgs) {
// what is being selected
String[] selection = {"COUNT(*)"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
tableName, // The table to query
selection, // The array of columns to return (pass null to get all)
where, // The columns for the WHERE clause
whereArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort results
);
}
// get the count, if the count is missing then set it to 0
int count = c.moveToFirst() ? c.getInt(0) : 0;
c.close();
return count;
}
/**
*
* This is a convenience method for {@link #getCount(String, String, String[])} that will count
* all rows in the table.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param tableName The name of the table to get the count for
* @return the number of matching rows
*/
@WorkerThread
public int getCount(@NonNull String tableName) {
return getCount(tableName, null, null);
}
/**
* This function is used to enter the first entry into the statistics collector. The record in
* the database will need to be updated later on when the sync response has been received to
* include the rest of the content.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param uuid the unique identifier for the data record. just needs to be unique per device.
* @param peer the peer that the data is being gotten from
* @param totalSize the total number of bytes that are being sent to the peer
* @param type the protocol version that the entry is for.
* @return <code>true</code> if it was inserted successfully, <code>false</code> otherwise
*/
@WorkerThread
public boolean logSync(@NonNull String uuid, @NonNull String peer, int totalSize, @IntRange(from = 0, to = 3) int type) {
ContentValues values = new ContentValues();
values.put(SyncStatsEntry.COLUMN_DATA_SEND_ID, uuid);
values.put(SyncStatsEntry.COLUMN_PEER_ID, peer);
values.put(SyncStatsEntry.COLUMN_PACKET_SIZE, totalSize);
values.put(SyncStatsEntry.COLUMN_MESSAGE_TYPE, type);
values.put(SyncStatsEntry.COLUMN_TIMESTAMP_SENT, new Date().getTime());
// note this is a potentially long running operation.
long id;
synchronized (this) {
id = getWritableDatabase().insert(SyncStatsEntry.TABLE_NAME, null, values);
}
// check if the creation was successful, return the user if it was
return id != -1;
}
/**
* This function is called after the response to the data sync request is sent. This will update
* when the data was received to check the time it takes, as well as capturing any lost messages.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
* @param uuid the unique ID for the data request
* @param additionalSize the additional size of the messages that was sent across the network
* @param numRecords the number of records that are being inserted
* @return <code>true</code> if the update was successful, <code>false</code> otherwise
*/
@WorkerThread
public boolean updateLogSync(@NonNull String uuid, int additionalSize, int numRecords) {
String selection = SyncStatsEntry.COLUMN_DATA_SEND_ID + " = ?";
String[] selectionArgs = { uuid };
String[] projection = { SyncStatsEntry.COLUMN_PACKET_SIZE };
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
int currentSize = 0;
// get the size of the data that was already sent
while(cursor.moveToNext()) {
currentSize = cursor.getInt(cursor.getColumnIndexOrThrow(SyncStatsEntry.COLUMN_PACKET_SIZE));
}
cursor.close();
ContentValues values = new ContentValues();
values.put(SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED, new Date().getTime());
values.put(SyncStatsEntry.COLUMN_NUM_RECORDS, numRecords);
values.put(SyncStatsEntry.COLUMN_PACKET_SIZE, currentSize + additionalSize);
int numUpdated;
synchronized (this) {
numUpdated = getWritableDatabase().update(
SyncStatsEntry.TABLE_NAME,
values,
selection,
selectionArgs
);
}
return numUpdated == 1;
}
/**
* This function is used for the stats page to show the total number of messages that get sent
* with this protocol version.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the total number of data request messages sent
*/
@WorkerThread
public int getNumMessages(@IntRange(from = 0, to = 3) int protocolVersion) {
String where = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ?";
String[] whereArgs = {String.valueOf(protocolVersion)};
return getCount(SyncStatsEntry.TABLE_NAME, where, whereArgs);
}
/**
* This function is used for the stats page to show the total size of all messages that have been
* sent, this will return the number of bytes that have been sent / received.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the total number of bytes of data sent during the sync messages
*/
@WorkerThread
public int getTotalMessageSize(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ?";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { "SUM(" + SyncStatsEntry.COLUMN_PACKET_SIZE + ")"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
int totalSize = c.moveToFirst() ? c.getInt(0) : 0;
c.close();
return totalSize;
}
/**
* This function is used for the stats page to show the average message size for all sent / received
* messages.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the average number of bytes that are sent
*/
@WorkerThread
public float getAverageMessageSize(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ? AND " +
SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " not NULL";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { "AVG(" + SyncStatsEntry.COLUMN_PACKET_SIZE + ")"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
float averageSize = c.moveToFirst() ? c.getFloat(0) : 0;
c.close();
return averageSize;
}
/**
* This function is used for the stats page to show the total number of records that have been
* received from peers during syncs.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the total number data records recived
*/
@WorkerThread
public int getTotalEntries(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ?";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { "SUM(" + SyncStatsEntry.COLUMN_NUM_RECORDS + ")"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
int numRecords = c.moveToFirst() ? c.getInt(0) : 0;
c.close();
return numRecords;
}
/**
* This function is used for the stats page to show the total number of records that are
* currently being stored.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @return the total number of records stored
*/
@WorkerThread
public int getTotalEntries() {
return getCount(KnownPostsEntry.TABLE_NAME);
}
/**
* This function is used for the stats page to show the total number of peers that this device
* has synced with. Note that the sync does not need to have been successful.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the total number of peers that it has performed a sync with.
*/
@WorkerThread
public int getTotalNumPeers(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ?";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { SyncStatsEntry.COLUMN_PEER_ID };
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
true, //distinct
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null, // don't sort
null
);
}
int numPeers = c.getCount();
c.close();
return numPeers;
}
/**
* This function is used for the stats page to show the longest time that it has taken to
* perform a sync. This does not include syncs that were not completed.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the slowest time to perform a data sync
*/
@WorkerThread
public double getSlowestTime(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ? AND " +
SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " not NULL";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { "MAX(" + SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " - " + SyncStatsEntry.COLUMN_TIMESTAMP_SENT + ")"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
long longestTime = c.moveToFirst() ? c.getLong(0) : 0;
c.close();
return (double)longestTime / 1000.0;
}
/**
* This function is used for the stats page to show the shortest time that it has taken to
* perform a sync. This does not include syncs that were not completed.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the fastest time to perform a data sync
*/
@WorkerThread
public double getFastestTime(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ? AND " +
SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " not NULL";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { "MIN(" + SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " - " + SyncStatsEntry.COLUMN_TIMESTAMP_SENT + ")"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
long shortestTime = c.moveToFirst() ? c.getLong(0) : 0;
c.close();
return (double) shortestTime / 1000.0;
}
/**
* This function is used for the stats page to show the average time that it takes to
* perform a sync. This does not include syncs that were not completed.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the average time to perform a data sync
*/
@WorkerThread
public double getAverageTime(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ? AND " +
SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " not NULL";
String[] selectionArgs = {String.valueOf(protocolVersion)};
String[] projection = { "AVG(" + SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " - " + SyncStatsEntry.COLUMN_TIMESTAMP_SENT + ")"};
Cursor c;
synchronized (this) {
c = getReadableDatabase().query(
SyncStatsEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // don't sort
);
}
double avgTime = c.moveToFirst() ? c.getDouble(0) : 0;
c.close();
return avgTime / 1000;
}
/**
* This function is used for the stats page to show the number of packets that were lost. This
* counts all of the sync requests that got sent that never got back a response for whatever reason.
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param protocolVersion the protocol version to check see {@link SyncStatsContract#SYNC_MESSAGE_V1}
* @return the number of lost sync messages
*/
@WorkerThread
public int getNumLost(@IntRange(from = 0, to = 3) int protocolVersion) {
String selection = SyncStatsEntry.COLUMN_MESSAGE_TYPE + " = ? AND " +
SyncStatsEntry.COLUMN_TIMESTAMP_RECEIVED + " is NULL";
String[] selectionArgs = {String.valueOf(protocolVersion)};
return getCount(SyncStatsEntry.TABLE_NAME, selection, selectionArgs);
}
/**
* Updates the status for the post and its following comments
* 0 = normal
* 1 = protected
* 2 = dead
*
* @param postHash the hash of the post item being marked
* @param status 0,1,2 as describe in the comment
* @return true if it updates successfully, false if something unexpected happens
*/
@WorkerThread
public boolean setPostStatus(String postHash, int status) {
String selection = KnownPostsEntry.COLUMN_POST_HASH + " = ? ";
String[] selectionArgs = { postHash };
ContentValues values = new ContentValues();
values.put(KnownPostsEntry.COLUMN_STATUS, status);
int numUpdated;
synchronized (this) {
numUpdated = getWritableDatabase().update(
KnownPostsEntry.TABLE_NAME,
values,
selection,
selectionArgs
);
}
return numUpdated == 1;
}
/**
* Returns the post status as
* 0 = normal
* 1 = protected
* 2 = dead
*
* @param postHash the hash of the post you wish to check
* @return
*/
public int getPostStatus(String postHash) {
String[] projection = {KnownPostsEntry.COLUMN_STATUS};
String selection = KnownPostsEntry.COLUMN_POST_HASH + " = ? AND " +
KnownPostsEntry.COLUMN_COMMENT_HASH + " == \"\" ";
String[] selectionArgs = {postHash};
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
KnownPostsEntry.TABLE_NAME, //Table to query
projection, // Array of columns to return
selection, // The columns for the WHERE clause
selectionArgs, // the values for the WHERE clause
null, // no row grouping
null, // no filter by row groups
null // don't sort
);
}
int status = 0;
while(cursor.moveToNext()) {
status = cursor.getInt(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_STATUS));
}
cursor.close();
return status;
}
/**
* This will delete all entries in the {@link KnownPostsContract} table.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*/
@WorkerThread
public void clearKnownPosts() {
getWritableDatabase().delete(KnownPostsEntry.TABLE_NAME, null, null);
}
/**
* This will delete all entries in the {@link SyncStatsContract} table.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*/
@WorkerThread
public void clearSyncStats() {
getWritableDatabase().delete(SyncStatsEntry.TABLE_NAME, null, null);
}
/**
* This will delete all entries in the {@link PeerListContract} table.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*/
@WorkerThread
public void clearPeers() {
getWritableDatabase().delete(SyncStatsEntry.TABLE_NAME, null, null);
}
/**
* This function will clear all posts and comments from the {@link KnownPostsContract} tables
* that have been marked as {@link KnownPostsContract#POST_DEAD}. This will not delete the actual
* post or comment from the hash table.
*
* Since this calls {@link #getWritableDatabase()}, do not call this from the main thread
*/
@WorkerThread
public void dataSaverClear(){
String whereClause = "status = " + KnownPostsContract.POST_DEAD;
getWritableDatabase().delete(KnownPostsEntry.TABLE_NAME, whereClause, null);
}
/**
* Gets all the posts and comment hashes based on their status. For the possible statuses see
* {@link KnownPostsContract#POST_NORMAL}
*
* Since this calls {@link #getReadableDatabase()}, do not call this from the main thread
* @param status indicates status 0, 1, or 2
* where
* 0 = normal {@link KnownPostsContract#POST_NORMAL}
* 1 = protected {@link KnownPostsContract#POST_PROTECTED}
* 2 = dead {@link KnownPostsContract#POST_DEAD}
* @return a {@link ArrayList} of the hashes to delete from the hash table
*/
@WorkerThread
public List<String> getAllHashesByStatus(int status){
String[] projection = {
KnownPostsEntry.COLUMN_POST_HASH,
KnownPostsEntry.COLUMN_COMMENT_HASH};
String selection = KnownPostsEntry.COLUMN_STATUS + " = ?";
String[] selectionArgs = {String.valueOf(status)};
Cursor cursor;
synchronized (this) {
cursor = getReadableDatabase().query(
true,
KnownPostsEntry.TABLE_NAME, //Table to query
projection, // Array of columns to return
selection, // The columns for the WHERE clause
selectionArgs, // the values for the WHERE clause
null,
null, // no row grouping
null, // no filter by row groups
null // don't sort
);
}
List<String> hashes = new ArrayList<>();
while(cursor.moveToNext()) {
String postHash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_POST_HASH));
String commentHash = cursor.getString(cursor.getColumnIndexOrThrow(KnownPostsEntry.COLUMN_COMMENT_HASH));
if (commentHash.isEmpty()) {
hashes.add(postHash);
} else {
hashes.add(commentHash);
}
}
cursor.close();
return hashes;
}
/**
* Gets the number of bytes in the file for the database.
* @return num bytes in the database file
*/
public long getSize() {
return context.getDatabasePath(DATABASE_NAME).length();
}
}