DB code rewrite

There's still a problem storing or retreiving messages
This commit is contained in:
Christian Basler 2015-08-19 07:01:30 +02:00
parent cb2040b0ce
commit 5e2d19df58
15 changed files with 933 additions and 19 deletions

View File

@ -28,10 +28,8 @@ dependencies {
compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-repositories:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT'
compile 'org.sqldroid:sqldroid:1.0.3'
compile 'org.slf4j:slf4j-android:1.7.12' compile 'org.slf4j:slf4j-android:1.7.12'
compile('com.mikepenz:materialdrawer:3.1.0@aar') { compile('com.mikepenz:materialdrawer:3.1.0@aar') {

View File

@ -1,5 +1,5 @@
CREATE TABLE Message ( CREATE TABLE Message (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
iv BINARY(32) UNIQUE, iv BINARY(32) UNIQUE,
type VARCHAR(20) NOT NULL, type VARCHAR(20) NOT NULL,
sender VARCHAR(40) NOT NULL, sender VARCHAR(40) NOT NULL,
@ -14,7 +14,7 @@ CREATE TABLE Message (
); );
CREATE TABLE Label ( CREATE TABLE Label (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
label VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL,
type VARCHAR(20), type VARCHAR(20),
color INT NOT NULL DEFAULT 4278190080, -- FF000000 color INT NOT NULL DEFAULT 4278190080, -- FF000000
@ -36,5 +36,6 @@ CREATE TABLE Message_Label (
INSERT INTO Label(label, type, color, ord) VALUES ('Inbox', 'INBOX', 4278190335, 0); INSERT INTO Label(label, type, color, ord) VALUES ('Inbox', 'INBOX', 4278190335, 0);
INSERT INTO Label(label, type, color, ord) VALUES ('Drafts', 'DRAFT', 4294940928, 10); INSERT INTO Label(label, type, color, ord) VALUES ('Drafts', 'DRAFT', 4294940928, 10);
INSERT INTO Label(label, type, color, ord) VALUES ('Sent', 'SENT', 4294967040, 20); INSERT INTO Label(label, type, color, ord) VALUES ('Sent', 'SENT', 4294967040, 20);
INSERT INTO Label(label, type, ord) VALUES ('Broadcast', 'BROADCAST', 50);
INSERT INTO Label(label, type, ord) VALUES ('Unread', 'UNREAD', 90); INSERT INTO Label(label, type, ord) VALUES ('Unread', 'UNREAD', 90);
INSERT INTO Label(label, type, ord) VALUES ('Trash', 'TRASH', 100); INSERT INTO Label(label, type, ord) VALUES ('Trash', 'TRASH', 100);

View File

@ -166,6 +166,8 @@ public class MessageListActivity extends AppCompatActivity
public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long l, IDrawerItem item) { public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long l, IDrawerItem item) {
if (item.getTag() instanceof Label) { if (item.getTag() instanceof Label) {
selectedLabel = (Label) item.getTag(); selectedLabel = (Label) item.getTag();
((MessageListFragment) getSupportFragmentManager()
.findFragmentById(R.id.message_list)).updateList(selectedLabel);
} else if (item instanceof Nameable<?>) { } else if (item instanceof Nameable<?>) {
Nameable<?> ni = (Nameable<?>) item; Nameable<?> ni = (Nameable<?>) item;
switch (ni.getNameRes()) { switch (ni.getNameRes()) {
@ -203,11 +205,16 @@ public class MessageListActivity extends AppCompatActivity
case R.id.sync_disabled: case R.id.sync_disabled:
bmc.startup(new BitmessageContext.Listener() { bmc.startup(new BitmessageContext.Listener() {
@Override @Override
public void receive(Plaintext plaintext) { public void receive(final Plaintext plaintext) {
// TODO // TODO
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MessageListActivity.this, plaintext.getSubject(), Toast.LENGTH_LONG).show(); Toast.makeText(MessageListActivity.this, plaintext.getSubject(), Toast.LENGTH_LONG).show();
} }
}); });
}
});
updateMenu(); updateMenu();
return true; return true;
case R.id.sync_enabled: case R.id.sync_enabled:

View File

@ -13,6 +13,7 @@ import android.widget.ListView;
import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label;
/** /**
* A list fragment representing a list of Messages. This fragment * A list fragment representing a list of Messages. This fragment
@ -79,12 +80,15 @@ public class MessageListFragment extends ListFragment {
bmc = Singleton.getBitmessageContext(getActivity()); bmc = Singleton.getBitmessageContext(getActivity());
// TODO: replace with a real list adapter. updateList(((MessageListActivity) getActivity()).getSelectedLabel());
}
public void updateList(Label label) {
setListAdapter(new ArrayAdapter<>( setListAdapter(new ArrayAdapter<>(
getActivity(), getActivity(),
android.R.layout.simple_list_item_activated_1, android.R.layout.simple_list_item_activated_1,
android.R.id.text1, android.R.id.text1,
bmc.messages().findMessages(((MessageListActivity) getActivity()).getSelectedLabel()))); bmc.messages().findMessages(label)));
} }
@Override @Override

View File

@ -0,0 +1,239 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repositories;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.utils.Encode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* {@link AddressRepository} implementation using the Android SQL API.
*/
public class AndroidAddressRepository implements AddressRepository {
private static final Logger LOG = LoggerFactory.getLogger(AndroidAddressRepository.class);
private static final String TABLE_NAME = "Address";
private static final String COLUMN_ADDRESS = "address";
private static final String COLUMN_VERSION = "version";
private static final String COLUMN_ALIAS = "alias";
private static final String COLUMN_PUBLIC_KEY = "public_key";
private static final String COLUMN_PRIVATE_KEY = "private_key";
private static final String COLUMN_SUBSCRIBED = "subscribed";
private final SqlHelper sql;
public AndroidAddressRepository(SqlHelper sql) {
this.sql = sql;
}
@Override
public BitmessageAddress findContact(byte[] ripeOrTag) {
for (BitmessageAddress address : find("public_key is null")) {
if (address.getVersion() > 3) {
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
} else {
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
}
}
return null;
}
@Override
public BitmessageAddress findIdentity(byte[] ripeOrTag) {
for (BitmessageAddress address : find("private_key is not null")) {
if (address.getVersion() > 3) {
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
} else {
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
}
}
return null;
}
@Override
public List<BitmessageAddress> getIdentities() {
return find("private_key IS NOT NULL");
}
@Override
public List<BitmessageAddress> getSubscriptions() {
return find("subscribed = '1'");
}
@Override
public List<BitmessageAddress> getSubscriptions(long broadcastVersion) {
if (broadcastVersion > 4) {
return find("subscribed = '1' AND version > 3");
} else {
return find("subscribed = '1' AND version <= 3");
}
}
@Override
public List<BitmessageAddress> getContacts() {
return find("private_key IS NULL");
}
private List<BitmessageAddress> find(String where) {
List<BitmessageAddress> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_ADDRESS,
COLUMN_ALIAS,
COLUMN_PUBLIC_KEY,
COLUMN_PRIVATE_KEY,
COLUMN_SUBSCRIBED
};
try {
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null, null
);
c.moveToFirst();
while (!c.isAfterLast()) {
BitmessageAddress address;
byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY));
if (privateKeyBytes != null) {
PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream(privateKeyBytes));
address = new BitmessageAddress(privateKey);
} else {
address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS)));
byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY));
if (publicKeyBytes != null) {
Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(),
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, false);
if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
pubkey = new V4Pubkey((V3Pubkey) pubkey);
}
address.setPubkey(pubkey);
}
}
address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS)));
address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1);
result.add(address);
c.moveToNext();
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return result;
}
@Override
public void save(BitmessageAddress address) {
try {
if (exists(address)) {
update(address);
} else {
insert(address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
private boolean exists(BitmessageAddress address) {
SQLiteDatabase db = sql.getReadableDatabase();
Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'", null);
cursor.moveToFirst();
return cursor.getInt(0) > 0;
}
private void update(BitmessageAddress address) throws IOException {
try {
SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(COLUMN_ALIAS, address.getAlias());
values.put(COLUMN_PUBLIC_KEY, Encode.bytes(address.getPubkey()));
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + "'", null);
if (update < 0) {
LOG.error("Could not update address " + address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
private void insert(BitmessageAddress address) throws IOException {
try {
SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(COLUMN_ADDRESS, address.getAddress());
values.put(COLUMN_VERSION, address.getVersion());
values.put(COLUMN_ALIAS, address.getAlias());
if (address.getPubkey() != null) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
address.getPubkey().writeUnencrypted(out);
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
} else {
values.put(COLUMN_PUBLIC_KEY, (byte[]) null);
}
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
long insert = db.insert(TABLE_NAME, null, values);
if (insert < 0) {
LOG.error("Could not insert address " + address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
@Override
public void remove(BitmessageAddress address) {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "address = " + address.getAddress(), null);
}
@Override
public BitmessageAddress getAddress(String address) {
List<BitmessageAddress> result = find("address = '" + address + "'");
if (result.size() > 0) return result.get(0);
return null;
}
}

View File

@ -1,7 +1,185 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repositories; package ch.dissem.apps.abit.repositories;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.Inventory;
import ch.dissem.bitmessage.utils.Encode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static ch.dissem.apps.abit.repositories.SqlHelper.join;
import static ch.dissem.bitmessage.utils.UnixTime.now;
/** /**
* Created by chris on 14.08.15. * {@link Inventory} implementation using the Android SQL API.
*/ */
public class AndroidInventory { public class AndroidInventory implements Inventory {
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
private static final String TABLE_NAME = "Inventory";
private static final String COLUMN_HASH = "hash";
private static final String COLUMN_STREAM = "stream";
private static final String COLUMN_EXPIRES = "expires";
private static final String COLUMN_DATA = "data";
private static final String COLUMN_TYPE = "type";
private static final String COLUMN_VERSION = "version";
private final SqlHelper sql;
public AndroidInventory(SqlHelper sql) {
this.sql = sql;
}
@Override
public List<InventoryVector> getInventory(long... streams) {
return getInventory(false, streams);
}
public List<InventoryVector> getInventory(boolean includeExpired, long... streams) {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_HASH
};
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
TABLE_NAME, projection,
(includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join(streams) + ")",
null, null, null, null
);
c.moveToFirst();
List<InventoryVector> result = new LinkedList<>();
while (!c.isAfterLast()) {
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
result.add(new InventoryVector(blob));
c.moveToNext();
}
return result;
}
@Override
public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) {
offer.removeAll(getInventory(true, streams));
return offer;
}
@Override
public ObjectMessage getObject(InventoryVector vector) {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_VERSION,
COLUMN_DATA
};
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
TABLE_NAME, projection,
"hash = X'" + vector + "'",
null, null, null, null
);
c.moveToFirst();
if (c.isAfterLast()) {
LOG.info("Object requested that we don't have. IV: " + vector);
return null;
}
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length);
}
@Override
public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_VERSION,
COLUMN_DATA
};
StringBuilder where = new StringBuilder("1=1");
if (stream > 0) {
where.append(" AND stream = ").append(stream);
}
if (version > 0) {
where.append(" AND version = ").append(version);
}
if (types.length > 0) {
where.append(" AND type IN (").append(join(types)).append(")");
}
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
TABLE_NAME, projection,
where.toString(),
null, null, null, null
);
c.moveToFirst();
List<ObjectMessage> result = new LinkedList<>();
while (!c.isAfterLast()) {
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION));
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), blob.length));
c.moveToNext();
}
return result;
}
@Override
public void storeObject(ObjectMessage object) {
InventoryVector iv = object.getInventoryVector();
LOG.trace("Storing object " + iv);
try {
SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(COLUMN_HASH, object.getInventoryVector().getHash());
values.put(COLUMN_STREAM, object.getStream());
values.put(COLUMN_EXPIRES, object.getExpiresTime());
values.put(COLUMN_DATA, Encode.bytes(object));
values.put(COLUMN_TYPE, object.getType());
values.put(COLUMN_VERSION, object.getVersion());
long insert = db.insert(TABLE_NAME, null, values);
if (insert < 0) {
LOG.trace("Error while inserting object. Most probably it was requested twice.");
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
@Override
public void cleanup() {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "expires < " + (now() - 300), null);
}
} }

View File

@ -0,0 +1,297 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repositories;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import ch.dissem.apps.abit.R;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Encode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static ch.dissem.apps.abit.repositories.SqlHelper.join;
/**
* {@link MessageRepository} implementation using the Android SQL API.
*/
public class AndroidMessageRepository implements MessageRepository, InternalContext.ContextHolder {
private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class);
private static final String TABLE_NAME = "Message";
private static final String COLUMN_ID = "id";
private static final String COLUMN_IV = "iv";
private static final String COLUMN_TYPE = "type";
private static final String COLUMN_SENDER = "sender";
private static final String COLUMN_RECIPIENT = "recipient";
private static final String COLUMN_DATA = "data";
private static final String COLUMN_SENT = "sent";
private static final String COLUMN_RECEIVED = "received";
private static final String COLUMN_STATUS = "status";
private static final String JOIN_TABLE_NAME = "Message_Label";
private static final String JT_COLUMN_MESSAGE = "message_id";
private static final String JT_COLUMN_LABEL = "label_id";
private static final String LBL_TABLE_NAME = "Label";
private static final String LBL_COLUMN_ID = "id";
private static final String LBL_COLUMN_LABEL = "label";
private static final String LBL_COLUMN_TYPE = "type";
private static final String LBL_COLUMN_COLOR = "color";
private static final String LBL_COLUMN_ORDER = "ord";
private final SqlHelper sql;
private final Context ctx;
private InternalContext bmc;
public AndroidMessageRepository(SqlHelper sql, Context ctx) {
this.sql = sql;
this.ctx = ctx;
}
@Override
public void setContext(InternalContext context) {
bmc = context;
}
@Override
public List<Label> getLabels() {
return findLabels(null);
}
@Override
public List<Label> getLabels(Label.Type... types) {
return findLabels("type IN (" + join(types) + ")");
}
public List<Label> findLabels(String where) {
List<Label> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
LBL_COLUMN_ID,
LBL_COLUMN_LABEL,
LBL_COLUMN_TYPE,
LBL_COLUMN_COLOR
};
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
LBL_TABLE_NAME, projection,
where,
null, null, null,
LBL_COLUMN_ORDER
);
c.moveToFirst();
while (!c.isAfterLast()) {
result.add(getLabel(c));
c.moveToNext();
}
return result;
}
private Label getLabel(Cursor c) {
String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE));
Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName);
String text;
switch (type) {
case INBOX:
text = ctx.getString(R.string.inbox);
break;
case DRAFT:
text = ctx.getString(R.string.draft);
break;
case SENT:
text = ctx.getString(R.string.sent);
break;
case UNREAD:
text = ctx.getString(R.string.unread);
break;
case TRASH:
text = ctx.getString(R.string.trash);
break;
case BROADCAST:
text = ctx.getString(R.string.broadcast);
break;
default:
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
}
Label label = new Label(
text,
type,
c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR)));
label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID)));
return label;
}
@Override
public List<Plaintext> findMessages(Label label) {
if (label != null) {
return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")");
} else {
return find("id NOT IN (SELECT message_id FROM Message_Label)");
}
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) {
return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'");
}
@Override
public List<Plaintext> findMessages(Plaintext.Status status) {
return find("status='" + status.name() + "'");
}
private List<Plaintext> find(String where) {
List<Plaintext> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_ID,
COLUMN_IV,
COLUMN_TYPE,
COLUMN_SENDER,
COLUMN_RECIPIENT,
COLUMN_DATA,
COLUMN_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS
};
try {
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null, null
);
c.moveToFirst();
while (!c.isAfterLast()) {
byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV));
byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA));
Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE)));
Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new ByteArrayInputStream(data));
long id = c.getLong(c.getColumnIndex(COLUMN_ID));
builder.id(id);
builder.IV(new InventoryVector(iv));
builder.from(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER))));
builder.to(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT))));
builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT)));
builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED)));
builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS))));
builder.labels(findLabels(id));
result.add(builder.build());
c.moveToNext();
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return result;
}
private Collection<Label> findLabels(long id) {
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
}
@Override
public void save(Plaintext message) {
try {
SQLiteDatabase db = sql.getWritableDatabase();
db.beginTransaction();
// save from address if necessary
if (message.getId() == null) {
BitmessageAddress savedAddress = bmc.getAddressRepo().getAddress(message.getFrom().getAddress());
if (savedAddress == null || savedAddress.getPrivateKey() == null) {
if (savedAddress != null && savedAddress.getAlias() != null) {
message.getFrom().setAlias(savedAddress.getAlias());
}
bmc.getAddressRepo().save(message.getFrom());
}
}
// save message
if (message.getId() == null) {
insert(db, message);
} else {
update(db, message);
}
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=" + message.getId(), null);
// save labels
ContentValues values = new ContentValues();
for (Label label : message.getLabels()) {
values.put(JT_COLUMN_LABEL, (Long) label.getId());
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
}
db.endTransaction();
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
private void insert(SQLiteDatabase db, Plaintext message) throws IOException {
ContentValues values = new ContentValues();
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash());
values.put(COLUMN_TYPE, message.getType().name());
values.put(COLUMN_SENDER, message.getFrom().getAddress());
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
values.put(COLUMN_DATA, Encode.bytes(message));
values.put(COLUMN_SENT, message.getSent());
values.put(COLUMN_RECEIVED, message.getReceived());
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
long id = db.insertOrThrow(TABLE_NAME, null, values);
message.setId(id);
}
private void update(SQLiteDatabase db, Plaintext message) throws IOException {
ContentValues values = new ContentValues();
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash());
values.put(COLUMN_TYPE, message.getType().name());
values.put(COLUMN_SENDER, message.getFrom().getAddress());
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
values.put(COLUMN_DATA, Encode.bytes(message));
values.put(COLUMN_SENT, message.getSent());
values.put(COLUMN_RECEIVED, message.getReceived());
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
}
@Override
public void remove(Plaintext message) {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "id = " + message.getId(), null);
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repositories;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import ch.dissem.apps.abit.utils.Assets;
/**
* Handles database migration and provides access.
*/
public class SqlHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version.
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "jabit.db";
protected final Context ctx;
public SqlHelper(Context ctx) {
super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
setWriteAheadLoggingEnabled(true);
this.ctx = ctx;
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, 1);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 0:
executeMigration(db, "V1.0__Create_table_inventory");
executeMigration(db, "V1.1__Create_table_address");
executeMigration(db, "V1.2__Create_table_message");
default:
// Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION.
}
}
protected void executeMigration(SQLiteDatabase db, String name) {
for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) {
db.execSQL(statement);
}
}
public static StringBuilder join(long... numbers) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < numbers.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(numbers[i]);
}
return streamList;
}
public static StringBuilder join(Enum<?>... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append('\'').append(types[i].name()).append('\'');
}
return streamList;
}
}

View File

@ -1,10 +1,13 @@
package ch.dissem.apps.abit.service; package ch.dissem.apps.abit.service;
import android.content.Context; import android.content.Context;
import ch.dissem.apps.abit.SQLiteConfig; import ch.dissem.apps.abit.repositories.AndroidAddressRepository;
import ch.dissem.apps.abit.repositories.AndroidInventory;
import ch.dissem.apps.abit.repositories.AndroidMessageRepository;
import ch.dissem.apps.abit.repositories.SqlHelper;
import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.ports.MemoryNodeRegistry;
import ch.dissem.bitmessage.security.sc.SpongySecurity; import ch.dissem.bitmessage.security.sc.SpongySecurity;
/** /**
@ -17,13 +20,14 @@ public class Singleton {
if (bitmessageContext == null) { if (bitmessageContext == null) {
synchronized (Singleton.class) { synchronized (Singleton.class) {
if (bitmessageContext == null) { if (bitmessageContext == null) {
JdbcConfig config = new SQLiteConfig(ctx); ctx = ctx.getApplicationContext();
SqlHelper sqlHelper = new SqlHelper(ctx);
bitmessageContext = new BitmessageContext.Builder() bitmessageContext = new BitmessageContext.Builder()
.security(new SpongySecurity()) .security(new SpongySecurity())
.nodeRegistry(new MemoryNodeRegistry()) .nodeRegistry(new MemoryNodeRegistry())
.inventory(new JdbcInventory(config)) .inventory(new AndroidInventory(sqlHelper))
.addressRepo(new JdbcAddressRepository(config)) .addressRepo(new AndroidAddressRepository(sqlHelper))
.messageRepo(new JdbcMessageRepository(config)) .messageRepo(new AndroidMessageRepository(sqlHelper, ctx))
.networkHandler(new DefaultNetworkHandler()) .networkHandler(new DefaultNetworkHandler())
.build(); .build();
} }

View File

@ -0,0 +1,56 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.utils;
import android.content.Context;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
/**
* Helper class to work with Assets.
*/
public class Assets {
public static String readToString(Context ctx, String name) {
try {
InputStream in = ctx.getAssets().open(name);
return new Scanner(in, "UTF-8").useDelimiter("\\A").next();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static List<String> readSqlStatements(Context ctx, String name) {
try {
InputStream in = ctx.getAssets().open(name);
Scanner scanner = new Scanner(in, "UTF-8").useDelimiter(";");
List<String> result = new LinkedList<>();
while (scanner.hasNext()) {
String statement = scanner.next().trim();
if (!"".equals(statement)) {
result.add(statement);
}
}
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -10,7 +10,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:elevation="4dp" /> android:elevation="4dp" />

View File

@ -9,7 +9,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:elevation="4dp" /> android:elevation="4dp" />

View File

@ -26,7 +26,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:elevation="4dp" android:elevation="4dp"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways"/> app:layout_scrollFlags="scroll|enterAlways"/>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2015 Christian Basler
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<string name="inbox">Posteingang</string>
<string name="draft">Entwürfe</string>
<string name="sent">Gesendet</string>
<string name="unread">Ungelesen</string>
<string name="trash">Papierkorb</string>
<string name="broadcast">Broadcasts</string>
</resources>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2015 Christian Basler
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<string name="inbox">Inbox</string>
<string name="draft">Drafts</string>
<string name="sent">Sent</string>
<string name="unread">Unread</string>
<string name="trash">Trash</string>
<string name="broadcast">Broadcasts</string>
</resources>