diff --git a/app/build.gradle b/app/build.gradle index 957eac4..cc59595 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,10 +28,8 @@ dependencies { 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-repositories: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('com.mikepenz:materialdrawer:3.1.0@aar') { diff --git a/app/src/main/assets/db/migration/V1.2__Create_table_message.sql b/app/src/main/assets/db/migration/V1.2__Create_table_message.sql index 03ffbbe..79e32f2 100644 --- a/app/src/main/assets/db/migration/V1.2__Create_table_message.sql +++ b/app/src/main/assets/db/migration/V1.2__Create_table_message.sql @@ -1,5 +1,5 @@ CREATE TABLE Message ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, iv BINARY(32) UNIQUE, type VARCHAR(20) NOT NULL, sender VARCHAR(40) NOT NULL, @@ -14,7 +14,7 @@ CREATE TABLE Message ( ); CREATE TABLE Label ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, label VARCHAR(255) NOT NULL, type VARCHAR(20), 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 ('Drafts', 'DRAFT', 4294940928, 10); 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 ('Trash', 'TRASH', 100); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index ec02e0c..5d6fb38 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -166,6 +166,8 @@ public class MessageListActivity extends AppCompatActivity public boolean onItemClick(AdapterView adapterView, View view, int i, long l, IDrawerItem item) { if (item.getTag() instanceof Label) { selectedLabel = (Label) item.getTag(); + ((MessageListFragment) getSupportFragmentManager() + .findFragmentById(R.id.message_list)).updateList(selectedLabel); } else if (item instanceof Nameable) { Nameable ni = (Nameable) item; switch (ni.getNameRes()) { @@ -203,9 +205,14 @@ public class MessageListActivity extends AppCompatActivity case R.id.sync_disabled: bmc.startup(new BitmessageContext.Listener() { @Override - public void receive(Plaintext plaintext) { + public void receive(final Plaintext plaintext) { // TODO - Toast.makeText(MessageListActivity.this, plaintext.getSubject(), Toast.LENGTH_LONG).show(); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(MessageListActivity.this, plaintext.getSubject(), Toast.LENGTH_LONG).show(); + } + }); } }); updateMenu(); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 9bcf399..64c06f4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -13,6 +13,7 @@ import android.widget.ListView; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; /** * A list fragment representing a list of Messages. This fragment @@ -79,12 +80,15 @@ public class MessageListFragment extends ListFragment { bmc = Singleton.getBitmessageContext(getActivity()); - // TODO: replace with a real list adapter. + updateList(((MessageListActivity) getActivity()).getSelectedLabel()); + } + + public void updateList(Label label) { setListAdapter(new ArrayAdapter<>( getActivity(), android.R.layout.simple_list_item_activated_1, android.R.id.text1, - bmc.messages().findMessages(((MessageListActivity) getActivity()).getSelectedLabel()))); + bmc.messages().findMessages(label))); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidAddressRepository.java new file mode 100644 index 0000000..6bcd280 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidAddressRepository.java @@ -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 getIdentities() { + return find("private_key IS NOT NULL"); + } + + @Override + public List getSubscriptions() { + return find("subscribed = '1'"); + } + + @Override + public List getSubscriptions(long broadcastVersion) { + if (broadcastVersion > 4) { + return find("subscribed = '1' AND version > 3"); + } else { + return find("subscribed = '1' AND version <= 3"); + } + } + + @Override + public List getContacts() { + return find("private_key IS NULL"); + } + + private List find(String where) { + List 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 result = find("address = '" + address + "'"); + if (result.size() > 0) return result.get(0); + return null; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java index 80d9bdd..8353138 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java @@ -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; +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 getInventory(long... streams) { + return getInventory(false, streams); + } + + public List 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 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 getMissing(List 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 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 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); + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidMessageRepository.java new file mode 100644 index 0000000..0b286c0 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidMessageRepository.java @@ -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