From af2bfc796b76ab594532a386c77f6397f52899a2 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Mon, 12 Sep 2016 11:00:00 +0200 Subject: [PATCH] Use the nio network listener. --- app/build.gradle | 10 +- .../db/migration/V3.3__Create_table_node.sql | 9 + .../apps/abit/ComposeMessageActivity.java | 1 + .../apps/abit/ComposeMessageFragment.java | 22 ++- .../apps/abit/MessageDetailFragment.java | 22 ++- .../repository/AndroidAddressRepository.java | 40 ++-- .../abit/repository/AndroidInventory.java | 47 ++--- .../repository/AndroidMessageRepository.java | 108 +++++----- .../abit/repository/AndroidNodeRegistry.java | 187 ++++++++++++++++++ .../AndroidProofOfWorkRepository.java | 69 ++++--- .../apps/abit/repository/SqlHelper.java | 10 +- .../dissem/apps/abit/service/Singleton.java | 11 +- build.gradle | 2 +- 13 files changed, 382 insertions(+), 156 deletions(-) create mode 100644 app/src/main/assets/db/migration/V3.3__Create_table_node.sql create mode 100644 app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java diff --git a/app/build.gradle b/app/build.gradle index e600ee0..3ba3317 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ if (project.hasProperty("project.configs") android { compileSdkVersion 24 - buildToolsVersion "24.0.1" + buildToolsVersion "24.0.2" defaultConfig { applicationId "ch.dissem.apps." + appName.toLowerCase() @@ -29,12 +29,12 @@ android { } } -ext.jabitVersion = 'develop-SNAPSHOT' +ext.jabitVersion = 'feature-nio-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:24.1.1' - compile 'com.android.support:support-v4:24.1.1' - compile 'com.android.support:design:24.1.1' + compile 'com.android.support:appcompat-v7:24.2.0' + compile 'com.android.support:support-v4:24.2.0' + compile 'com.android.support:design:24.2.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" diff --git a/app/src/main/assets/db/migration/V3.3__Create_table_node.sql b/app/src/main/assets/db/migration/V3.3__Create_table_node.sql new file mode 100644 index 0000000..5d03bb5 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.3__Create_table_node.sql @@ -0,0 +1,9 @@ +CREATE TABLE Node ( + stream BIGINT NOT NULL, + address BINARY(32) NOT NULL, + port INT NOT NULL, + services BIGINT NOT NULL, + time BIGINT NOT NULL, + PRIMARY KEY (stream, address, port) +); +CREATE INDEX idx_time on Node(time); diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 5fa12a4..c5f2032 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -27,6 +27,7 @@ public class ComposeMessageActivity extends AppCompatActivity { public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"; public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"; public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"; + public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index b19ef4d..5d80e31 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -18,6 +18,7 @@ package ch.dissem.apps.abit; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.text.Selection; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -34,6 +35,7 @@ import ch.dissem.apps.abit.adapter.ContactAdapter; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; @@ -45,6 +47,7 @@ public class ComposeMessageFragment extends Fragment { private BitmessageAddress identity; private BitmessageAddress recipient; private String subject; + private String content; private AutoCompleteTextView recipientInput; private EditText subjectInput; private EditText bodyInput; @@ -71,6 +74,9 @@ public class ComposeMessageFragment extends Fragment { if (getArguments().containsKey(EXTRA_SUBJECT)) { subject = getArguments().getString(EXTRA_SUBJECT); } + if (getArguments().containsKey(EXTRA_CONTENT)) { + content = getArguments().getString(EXTRA_CONTENT); + } } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } @@ -106,6 +112,16 @@ public class ComposeMessageFragment extends Fragment { subjectInput = (EditText) rootView.findViewById(R.id.subject); subjectInput.setText(subject); bodyInput = (EditText) rootView.findViewById(R.id.body); + bodyInput.setText(content); + + if (recipient == null) { + recipientInput.requestFocus(); + } else if (subject == null || subject.isEmpty()) { + subjectInput.requestFocus(); + } else { + bodyInput.requestFocus(); + bodyInput.setSelection(0); + } return rootView; } @@ -126,7 +142,7 @@ public class ComposeMessageFragment extends Fragment { recipient = new BitmessageAddress(inputString); } catch (Exception e) { List contacts = Singleton.getAddressRepository - (getContext()).getContacts(); + (getContext()).getContacts(); for (BitmessageAddress contact : contacts) { if (inputString.equalsIgnoreCase(contact.getAlias())) { recipient = contact; @@ -137,8 +153,8 @@ public class ComposeMessageFragment extends Fragment { } } Singleton.getBitmessageContext(getContext()).send(identity, recipient, - subjectInput.getText().toString(), - bodyInput.getText().toString()); + subjectInput.getText().toString(), + bodyInput.getText().toString()); getActivity().finish(); return true; default: diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index b67cee5..6afb2d0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -44,6 +44,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import static android.text.util.Linkify.WEB_URLS; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; @@ -100,7 +101,7 @@ public class MessageDetailFragment extends Fragment { ((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject()); BitmessageAddress sender = item.getFrom(); ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon - (sender)); + (sender)); ((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString()); if (item.getTo() != null) { ((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString()); @@ -112,11 +113,11 @@ public class MessageDetailFragment extends Fragment { Linkify.addLinks(messageBody, WEB_URLS); Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, - new TransformFilter() { - public final String transformUrl(final Matcher match, String url) { - return match.group(); - } - }); + new TransformFilter() { + public final String transformUrl(final Matcher match, String url) { + return match.group(); + } + }); messageBody.setLinksClickable(true); messageBody.setTextIsSelectable(true); @@ -146,7 +147,7 @@ public class MessageDetailFragment extends Fragment { Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply); Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon - .gmd_markunread); + .gmd_markunread); Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive); super.onCreateOptionsMenu(menu, inflater); @@ -158,17 +159,20 @@ public class MessageDetailFragment extends Fragment { switch (menuItem.getItemId()) { case R.id.reply: Intent replyIntent = new Intent(getActivity().getApplicationContext(), - ComposeMessageActivity.class); + ComposeMessageActivity.class); replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); String prefix; if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) - .equalsIgnoreCase("RE:")) { + .equalsIgnoreCase("RE:")) { prefix = ""; } else { prefix = "RE: "; } replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject()); + replyIntent.putExtra(EXTRA_CONTENT, + "\n\n------------------------------------------------------\n" + + item.getText()); startActivity(replyIntent); return true; case R.id.delete: diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 25c9b2a..4eb3374 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -119,37 +119,36 @@ public class AndroidAddressRepository implements AddressRepository { // 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, - COLUMN_CHAN + COLUMN_ADDRESS, + COLUMN_ALIAS, + COLUMN_PUBLIC_KEY, + COLUMN_PRIVATE_KEY, + COLUMN_SUBSCRIBED, + COLUMN_CHAN }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, null + TABLE_NAME, projection, + where, + null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { BitmessageAddress address; byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)); if (privateKeyBytes != null) { PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream - (privateKeyBytes)); + (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); + .getStream(), + new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, + false); if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) { pubkey = new V4Pubkey((V3Pubkey) pubkey); } @@ -161,7 +160,6 @@ public class AndroidAddressRepository implements AddressRepository { address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1); result.add(address); - c.moveToNext(); } } catch (IOException e) { LOG.error(e.getMessage(), e); @@ -184,8 +182,10 @@ public class AndroidAddressRepository implements AddressRepository { private boolean exists(BitmessageAddress address) { SQLiteDatabase db = sql.getReadableDatabase(); - try (Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address - .getAddress() + "'", null)) { + try (Cursor cursor = db.rawQuery( + "SELECT COUNT(*) FROM Address WHERE address=?", + new String[]{address.getAddress()} + )) { cursor.moveToFirst(); return cursor.getInt(0) > 0; } @@ -210,8 +210,8 @@ public class AndroidAddressRepository implements AddressRepository { values.put(COLUMN_CHAN, address.isChan()); values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); - int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + - "'", null); + int update = db.update(TABLE_NAME, values, "address=?", + new String[]{address.getAddress()}); if (update < 0) { LOG.error("Could not update address " + address); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index c88ca2a..8632679 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -25,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -42,6 +41,7 @@ import ch.dissem.bitmessage.utils.Encode; import static ch.dissem.apps.abit.repository.SqlHelper.join; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.now; +import static java.lang.String.valueOf; /** * {@link Inventory} implementation using the Android SQL API. @@ -88,21 +88,19 @@ public class AndroidInventory implements Inventory { cache.put(stream, result); String[] projection = { - COLUMN_HASH, COLUMN_EXPIRES + COLUMN_HASH, COLUMN_EXPIRES }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - "stream = " + stream, - null, null, null, null + TABLE_NAME, projection, + "stream = " + stream, + null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES)); result.put(new InventoryVector(blob), expires); - c.moveToNext(); } } LOG.info("Stream #" + stream + " inventory size: " + result.size()); @@ -126,18 +124,17 @@ public class AndroidInventory implements Inventory { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_VERSION, - COLUMN_DATA + COLUMN_VERSION, + COLUMN_DATA }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - "hash = X'" + vector + "'", - null, null, null, null + TABLE_NAME, projection, + "hash = X'" + vector + "'", + null, null, null, null )) { - c.moveToFirst(); - if (c.isAfterLast()) { + if (!c.moveToFirst()) { LOG.info("Object requested that we don't have. IV: " + vector); return null; } @@ -153,8 +150,8 @@ public class AndroidInventory implements Inventory { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_VERSION, - COLUMN_DATA + COLUMN_VERSION, + COLUMN_DATA }; StringBuilder where = new StringBuilder("1=1"); if (stream > 0) { @@ -170,17 +167,15 @@ public class AndroidInventory implements Inventory { SQLiteDatabase db = sql.getReadableDatabase(); List result = new LinkedList<>(); try (Cursor c = db.query( - TABLE_NAME, projection, - where.toString(), - null, null, null, null + TABLE_NAME, projection, + where.toString(), + null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { 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(); + blob.length)); } } return result; @@ -211,8 +206,6 @@ public class AndroidInventory implements Inventory { getCache(object.getStream()).put(iv, object.getExpiresTime()); } catch (SQLiteConstraintException e) { LOG.trace(e.getMessage(), e); - } catch (IOException e) { - LOG.error(e.getMessage(), e); } } @@ -225,7 +218,7 @@ public class AndroidInventory implements Inventory { public void cleanup() { long fiveMinutesAgo = now() - 5 * MINUTE; SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, "expires < " + fiveMinutesAgo, null); + db.delete(TABLE_NAME, "expires < ?", new String[]{valueOf(fiveMinutesAgo)}); for (Map c : cache.values()) { Iterator> iterator = c.entrySet().iterator(); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 5a1765e..f430f66 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -41,6 +41,8 @@ import ch.dissem.bitmessage.ports.AbstractMessageRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; +import static java.lang.String.valueOf; + /** * {@link MessageRepository} implementation using the Android SQL API. */ @@ -87,23 +89,21 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // 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 + LBL_COLUMN_ID, + LBL_COLUMN_LABEL, + LBL_COLUMN_TYPE, + LBL_COLUMN_COLOR }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - LBL_TABLE_NAME, projection, - where, - null, null, null, - LBL_COLUMN_ORDER + LBL_TABLE_NAME, projection, + where, + null, null, null, + LBL_COLUMN_ORDER )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { result.add(getLabel(c)); - c.moveToNext(); } } return result; @@ -140,26 +140,34 @@ public class AndroidMessageRepository extends AbstractMessageRepository { } } Label label = new Label( - text, - type, - c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR))); + text, + type, + c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR))); label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID))); return label; } @Override public int countUnread(Label label) { + String[] args; String where; if (label != null) { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() - + ") AND "; + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND "; + args = new String[]{ + label.getId().toString(), + Label.Type.UNREAD.name() + }; } else { where = ""; + args = new String[]{ + Label.Type.UNREAD.name() + }; } SQLiteDatabase db = sql.getReadableDatabase(); return (int) DatabaseUtils.queryNumEntries(db, TABLE_NAME, - where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + - "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))" + where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + + "SELECT id FROM Label WHERE type=?))", + args ); } @@ -169,48 +177,47 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // 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_ACK_DATA, - COLUMN_SENT, - COLUMN_RECEIVED, - COLUMN_STATUS, - COLUMN_TTL, - COLUMN_RETRIES, - COLUMN_NEXT_TRY + COLUMN_ID, + COLUMN_IV, + COLUMN_TYPE, + COLUMN_SENDER, + COLUMN_RECIPIENT, + COLUMN_DATA, + COLUMN_ACK_DATA, + COLUMN_SENT, + COLUMN_RECEIVED, + COLUMN_STATUS, + COLUMN_TTL, + COLUMN_RETRIES, + COLUMN_NEXT_TRY }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, - COLUMN_RECEIVED + " DESC" + TABLE_NAME, projection, + where, + null, null, null, + COLUMN_RECEIVED + " DESC" )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { 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))); + (COLUMN_TYPE))); Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new - ByteArrayInputStream(data)); + ByteArrayInputStream(data)); long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); builder.from(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex - (COLUMN_SENDER)))); + (COLUMN_SENDER)))); builder.to(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex - (COLUMN_RECIPIENT)))); + (COLUMN_RECIPIENT)))); builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))); 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)))); + (COLUMN_STATUS)))); builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL))); builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES))); int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY); @@ -219,7 +226,6 @@ public class AndroidMessageRepository extends AbstractMessageRepository { } builder.labels(findLabels(id)); result.add(builder.build()); - c.moveToNext(); } } catch (IOException e) { LOG.error(e.getMessage(), e); @@ -240,7 +246,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // save from address if necessary if (message.getId() == null) { BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message - .getFrom().getAddress()); + .getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); @@ -257,7 +263,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { } // remove existing labels - db.delete(JOIN_TABLE_NAME, "message_id=" + message.getId(), null); + db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())}); // save labels ContentValues values = new ContentValues(); @@ -279,15 +285,19 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private void insert(SQLiteDatabase db, Plaintext message) throws IOException { ContentValues values = new ContentValues(); values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message - .getInventoryVector().getHash()); + .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_ACK_DATA, message.getAckData()); values.put(COLUMN_SENT, message.getSent()); values.put(COLUMN_RECEIVED, message.getReceived()); values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); + values.put(COLUMN_TTL, message.getTTL()); + values.put(COLUMN_RETRIES, message.getRetries()); + values.put(COLUMN_NEXT_TRY, message.getNextTry()); long id = db.insertOrThrow(TABLE_NAME, null, values); message.setId(id); } @@ -295,15 +305,19 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private void update(SQLiteDatabase db, Plaintext message) throws IOException { ContentValues values = new ContentValues(); values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message - .getInventoryVector().getHash()); + .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_ACK_DATA, message.getAckData()); values.put(COLUMN_SENT, message.getSent()); values.put(COLUMN_RECEIVED, message.getReceived()); values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); + values.put(COLUMN_TTL, message.getTTL()); + values.put(COLUMN_RETRIES, message.getRetries()); + values.put(COLUMN_NEXT_TRY, message.getNextTry()); db.update(TABLE_NAME, values, "id = " + message.getId(), null); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java new file mode 100644 index 0000000..b20c173 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java @@ -0,0 +1,187 @@ +package ch.dissem.apps.abit.repository; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteConstraintException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteStatement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.ports.NodeRegistry; +import ch.dissem.bitmessage.utils.Collections; +import ch.dissem.bitmessage.utils.SqlStrings; + +import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes; +import static ch.dissem.bitmessage.utils.Strings.hex; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static ch.dissem.bitmessage.utils.UnixTime.now; +import static java.lang.String.valueOf; + +/** + * @author Christian Basler + */ +public class AndroidNodeRegistry implements NodeRegistry { + private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class); + + private static final String TABLE_NAME = "Node"; + private static final String COLUMN_STREAM = "stream"; + private static final String COLUMN_ADDRESS = "address"; + private static final String COLUMN_PORT = "port"; + private static final String COLUMN_SERVICES = "services"; + private static final String COLUMN_TIME = "time"; + + private final ThreadLocal loadExistingStatement = new ThreadLocal<>(); + + private final SqlHelper sql; + private Map> stableNodes; + + public AndroidNodeRegistry(SqlHelper sql) { + this.sql = sql; + cleanUp(); + } + + private void cleanUp() { + SQLiteDatabase db = sql.getWritableDatabase(); + db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))}); + } + + private Long loadExistingTime(NetworkAddress node) { + SQLiteStatement statement = loadExistingStatement.get(); + if (statement == null) { + statement = sql.getWritableDatabase().compileStatement( + "SELECT " + COLUMN_TIME + + " FROM " + TABLE_NAME + + " WHERE stream=? AND address=? AND port=?" + ); + loadExistingStatement.set(statement); + } + statement.bindLong(1, node.getStream()); + statement.bindBlob(2, node.getIPv6()); + statement.bindLong(3, node.getPort()); + try { + return statement.simpleQueryForLong(); + } catch (SQLiteDoneException e) { + return null; + } + } + + @Override + public List getKnownAddresses(int limit, long... streams) { + String[] projection = { + COLUMN_STREAM, + COLUMN_ADDRESS, + COLUMN_PORT, + COLUMN_SERVICES, + COLUMN_TIME + }; + + List result = new LinkedList<>(); + SQLiteDatabase db = sql.getReadableDatabase(); + try (Cursor c = db.query( + TABLE_NAME, projection, + "stream IN (?)", + new String[]{SqlStrings.join(streams).toString()}, + null, null, + "time DESC", + valueOf(limit) + )) { + while (c.moveToNext()) { + result.add( + new NetworkAddress.Builder() + .stream(c.getLong(c.getColumnIndex(COLUMN_STREAM))) + .ipv6(c.getBlob(c.getColumnIndex(COLUMN_ADDRESS))) + .port(c.getInt(c.getColumnIndex(COLUMN_PORT))) + .services(c.getLong(c.getColumnIndex(COLUMN_SERVICES))) + .time(c.getLong(c.getColumnIndex(COLUMN_TIME))) + .build() + ); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new ApplicationException(e); + } + if (result.isEmpty()) { + synchronized (this) { + if (stableNodes == null) { + stableNodes = loadStableNodes(); + } + } + for (long stream : streams) { + Set nodes = stableNodes.get(stream); + if (nodes != null && !nodes.isEmpty()) { + result.add(Collections.selectRandom(nodes)); + } + } + } + return result; + } + + @Override + public void offerAddresses(List nodes) { + SQLiteDatabase db = sql.getWritableDatabase(); + db.beginTransaction(); + try { + cleanUp(); + for (NetworkAddress node : nodes) { + if (node.getTime() < now(+5 * MINUTE) && node.getTime() > now(-28 * DAY)) { + synchronized (this) { + Long existing = loadExistingTime(node); + if (existing == null) { + insert(node); + } else if (node.getTime() > existing) { + update(node); + } + } + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void insert(NetworkAddress node) { + try { + SQLiteDatabase db = sql.getWritableDatabase(); + // Create a new map of values, where column names are the keys + ContentValues values = new ContentValues(); + values.put(COLUMN_STREAM, node.getStream()); + values.put(COLUMN_ADDRESS, node.getIPv6()); + values.put(COLUMN_PORT, node.getPort()); + values.put(COLUMN_SERVICES, node.getServices()); + values.put(COLUMN_TIME, node.getTime()); + + db.insertOrThrow(TABLE_NAME, null, values); + } catch (SQLiteConstraintException e) { + LOG.trace(e.getMessage(), e); + } + } + + private void update(NetworkAddress node) { + try { + SQLiteDatabase db = sql.getWritableDatabase(); + // Create a new map of values, where column names are the keys + ContentValues values = new ContentValues(); + values.put(COLUMN_SERVICES, node.getServices()); + values.put(COLUMN_TIME, node.getTime()); + + db.update(TABLE_NAME, values, + "stream=" + node.getStream() + " AND address=X'" + hex(node.getIPv6()) + "' AND " + + "port=" + node.getPort(), + null); + } catch (SQLiteConstraintException e) { + LOG.trace(e.getMessage(), e); + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index 5fccc5d..d27a821 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -25,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -37,12 +36,13 @@ import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Strings; import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static ch.dissem.bitmessage.utils.Strings.hex; /** * @author Christian Basler */ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, InternalContext - .ContextHolder { + .ContextHolder { private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class); private static final String TABLE_NAME = "POW"; @@ -71,46 +71,45 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_DATA, - COLUMN_VERSION, - COLUMN_NONCE_TRIALS_PER_BYTE, - COLUMN_EXTRA_BYTES, - COLUMN_EXPIRATION_TIME, - COLUMN_MESSAGE_ID + COLUMN_DATA, + COLUMN_VERSION, + COLUMN_NONCE_TRIALS_PER_BYTE, + COLUMN_EXTRA_BYTES, + COLUMN_EXPIRATION_TIME, + COLUMN_MESSAGE_ID }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - "initial_hash = X'" + Strings.hex(initialHash) + "'", - null, null, null, null + TABLE_NAME, projection, + "initial_hash=X'" + hex(initialHash) + "'", + null, null, null, null )) { - c.moveToFirst(); - if (!c.isAfterLast()) { + if (c.moveToFirst()) { int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) { return new Item( - Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob - .length), - c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), - c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) ); } else { return new Item( - Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob - .length), - c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), - c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), - c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), - bmc.getMessageRepository().getMessage( - c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), + c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), + bmc.getMessageRepository().getMessage( + c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) ); } } } throw new RuntimeException("Object requested that we don't have. Initial hash: " + - Strings.hex(initialHash)); + hex(initialHash)); } @Override @@ -118,20 +117,18 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_INITIAL_HASH + COLUMN_INITIAL_HASH }; SQLiteDatabase db = sql.getReadableDatabase(); List result = new LinkedList<>(); try (Cursor c = db.query( - TABLE_NAME, projection, - null, null, null, null, null + TABLE_NAME, projection, + null, null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)); result.add(initialHash); - c.moveToNext(); } } return result; @@ -156,8 +153,6 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte db.insertOrThrow(TABLE_NAME, null, values); } catch (SQLiteConstraintException e) { LOG.trace(e.getMessage(), e); - } catch (IOException e) { - LOG.error(e.getMessage(), e); } } @@ -169,8 +164,10 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte @Override public void removeObject(byte[] initialHash) { SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, - "initial_hash = X'" + Strings.hex(initialHash) + "'", - null); + db.delete( + TABLE_NAME, + "initial_hash=X'" + hex(initialHash) + "'", + null + ); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 9a0f7ec..87a2bc2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit.repository; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; + import ch.dissem.apps.abit.util.Assets; /** @@ -26,7 +27,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 4; + public static final int DATABASE_VERSION = 5; public static final String DATABASE_NAME = "jabit.db"; protected final Context ctx; @@ -38,7 +39,7 @@ public class SqlHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { - onUpgrade(db, 0, 2); + onUpgrade(db, 0, DATABASE_VERSION); } @Override @@ -56,8 +57,11 @@ public class SqlHelper extends SQLiteOpenHelper { case 3: executeMigration(db, "V3.1__Update_table_POW"); executeMigration(db, "V3.2__Update_table_message"); + case 4: + executeMigration(db, "V3.3__Create_table_node"); default: - // Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION. + // Nothing to do. Let's assume we won't upgrade from a version that's newer than + // DATABASE_VERSION. } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 178c8bb..1bc0f77 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -28,16 +28,17 @@ import ch.dissem.apps.abit.pow.ServerPowEngine; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; import ch.dissem.apps.abit.repository.AndroidMessageRepository; +import ch.dissem.apps.abit.repository.AndroidNodeRegistry; import ch.dissem.apps.abit.repository.AndroidProofOfWorkRepository; import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.apps.abit.util.Constants; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.TTL; import static ch.dissem.bitmessage.utils.UnixTime.DAY; @@ -58,6 +59,7 @@ public class Singleton { final Context ctx = context.getApplicationContext(); SqlHelper sqlHelper = new SqlHelper(ctx); powRepo = new AndroidProofOfWorkRepository(sqlHelper); + TTL.pubkey(2 * DAY); bitmessageContext = new BitmessageContext.Builder() .proofOfWorkEngine(new SwitchingProofOfWorkEngine( ctx, Constants.PREFERENCE_SERVER_POW, @@ -65,15 +67,14 @@ public class Singleton { new ServicePowEngine(ctx) )) .cryptography(new AndroidCryptography()) - .nodeRegistry(new MemoryNodeRegistry()) + .nodeRegistry(new AndroidNodeRegistry(sqlHelper)) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) .powRepo(powRepo) - .networkHandler(new DefaultNetworkHandler()) + .networkHandler(new NioNetworkHandler()) .listener(getMessageListener(ctx)) .doNotSendPubkeyOnIdentityCreation() - .pubkeyTTL(2 * DAY) .build(); } } diff --git a/build.gradle b/build.gradle index e8ad5dc..de60887 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files