diff --git a/app/build.gradle b/app/build.gradle index 4e7094f..01ba6f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,7 @@ 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-security-spongy:0.2.1-SNAPSHOT' + compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT' compile 'org.slf4j:slf4j-android:1.7.12' 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 79e32f2..ae72a6f 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 @@ -8,6 +8,7 @@ CREATE TABLE Message ( sent INTEGER, received INTEGER, status VARCHAR(20) NOT NULL, + initial_hash BINARY(64) UNIQUE, FOREIGN KEY (sender) REFERENCES Address (address), FOREIGN KEY (recipient) REFERENCES Address (address) diff --git a/app/src/main/assets/db/migration/V2.0__Update_table_message.sql b/app/src/main/assets/db/migration/V2.0__Update_table_message.sql new file mode 100644 index 0000000..2b39a6e --- /dev/null +++ b/app/src/main/assets/db/migration/V2.0__Update_table_message.sql @@ -0,0 +1,7 @@ +-- This is done in V1.2, as SQLite doesn't support ADD CONSTRAINT and a proper migration +-- wasn't really necessary yet. +-- +-- This file is here to reduce confusion regarding to the original migration files. + +--ALTER TABLE Message ADD COLUMN initial_hash BINARY(64); +--ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash); \ No newline at end of file diff --git a/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql b/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql new file mode 100644 index 0000000..b39c6c5 --- /dev/null +++ b/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql @@ -0,0 +1,7 @@ +CREATE TABLE POW ( + initial_hash BINARY(64) PRIMARY KEY, + data BLOB NOT NULL, + version BIGINT NOT NULL, + nonce_trials_per_byte BIGINT NOT NULL, + extra_bytes BIGINT NOT NULL +); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 6f444ad..5d7bee8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -50,6 +50,8 @@ import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; +import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -85,7 +87,6 @@ public class MainActivity extends AppCompatActivity public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); - private static final long SYNC_FREQUENCY = 15 * 60; // seconds private static final int ADD_IDENTITY = 1; /** @@ -134,7 +135,8 @@ public class MainActivity extends AppCompatActivity setSupportActionBar(toolbar); MessageListFragment listFragment = new MessageListFragment(); - getSupportFragmentManager().beginTransaction().replace(R.id.item_list, listFragment).commit(); + getSupportFragmentManager().beginTransaction().replace(R.id.item_list, listFragment) + .commit(); if (findViewById(R.id.message_detail_container) != null) { // The detail container view will be present only in the @@ -157,21 +159,10 @@ public class MainActivity extends AppCompatActivity onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE)); } - createSyncAccount(); - } - - private void createSyncAccount() { - // Create account, if it's missing. (Either first run, or user has deleted account.) - Account account = new Account(Authenticator.ACCOUNT_NAME, Authenticator.ACCOUNT_TYPE); - - if (AccountManager.get(this).addAccountExplicitly(account, null, null)) { - // Inform the system that this account supports sync - ContentResolver.setIsSyncable(account, AUTHORITY, 1); - // Inform the system that this account is eligible for auto sync when the network is up - ContentResolver.setSyncAutomatically(account, AUTHORITY, true); - // Recommend a schedule for automatic synchronization. The system may modify this based - // on other scheduled syncs and network utilization. - ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); + if (Preferences.useTrustedNode(this)) { + SyncAdapter.startSync(this); + } else { + SyncAdapter.stopSync(this); } } @@ -220,10 +211,12 @@ public class MainActivity extends AppCompatActivity .withProfiles(profiles) .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { @Override - public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { + public boolean onProfileChanged(View view, IProfile profile, boolean + currentProfile) { if (profile.getIdentifier() == ADD_IDENTITY) { try { - Message message = Message.obtain(null, BitmessageService.MSG_CREATE_IDENTITY); + Message message = Message.obtain(null, BitmessageService + .MSG_CREATE_IDENTITY); message.replyTo = messenger; service.send(message); } catch (RemoteException e) { @@ -240,14 +233,15 @@ public class MainActivity extends AppCompatActivity } }) .build(); - if (profiles.size() > 0) { + if (profiles.size() > 2) { // There's always the add and manage identity items accountHeader.setActiveProfile(profiles.get(0), true); } incomingHandler.updateAccountHeader(accountHeader); ArrayList drawerItems = new ArrayList<>(); for (Label label : labels) { - PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag(label); + PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag + (label); if (label.getType() == null) { item.withIcon(CommunityMaterial.Icon.cmd_label); } else { @@ -301,17 +295,21 @@ public class MainActivity extends AppCompatActivity .withChecked(BitmessageService.isRunning()) .withOnCheckedChangeListener(new OnCheckedChangeListener() { @Override - public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { + public void onCheckedChanged(IDrawerItem drawerItem, + CompoundButton buttonView, + boolean isChecked) { if (messenger != null) { if (isChecked) { try { - service.send(Message.obtain(null, MSG_START_NODE)); + service.send(Message.obtain(null, + MSG_START_NODE)); } catch (RemoteException e) { LOG.error(e.getMessage(), e); } } else { try { - service.send(Message.obtain(null, MSG_STOP_NODE)); + service.send(Message.obtain(null, + MSG_STOP_NODE)); } catch (RemoteException e) { LOG.error(e.getMessage(), e); } @@ -322,7 +320,8 @@ public class MainActivity extends AppCompatActivity ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override - 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) { selectedLabel = (Label) item.getTag(); showSelectedLabel(); @@ -331,15 +330,18 @@ public class MainActivity extends AppCompatActivity Nameable ni = (Nameable) item; switch (ni.getNameRes()) { case R.string.contacts_and_subscriptions: - if (!(getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof SubscriptionListFragment)) { + if (!(getSupportFragmentManager().findFragmentById(R.id + .item_list) instanceof SubscriptionListFragment)) { changeList(new SubscriptionListFragment()); } else { ((SubscriptionListFragment) getSupportFragmentManager() .findFragmentById(R.id.item_list)).updateList(); } + break; case R.string.settings: - startActivity(new Intent(MainActivity.this, SettingsActivity.class)); + startActivity(new Intent(MainActivity.this, SettingsActivity + .class)); break; case R.string.archive: selectedLabel = null; @@ -357,7 +359,8 @@ public class MainActivity extends AppCompatActivity } private void showSelectedLabel() { - if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { + if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof + MessageListFragment) { ((MessageListFragment) getSupportFragmentManager() .findFragmentById(R.id.item_list)).updateList(selectedLabel); } else { @@ -385,7 +388,8 @@ public class MainActivity extends AppCompatActivity else if (item instanceof BitmessageAddress) fragment = new SubscriptionDetailFragment(); else - throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but was " + throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + + "was " + item.getClass().getSimpleName()); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() @@ -400,7 +404,8 @@ public class MainActivity extends AppCompatActivity else if (item instanceof BitmessageAddress) detailIntent = new Intent(this, SubscriptionDetailActivity.class); else - throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but was " + throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + + "was " + item.getClass().getSimpleName()); detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); @@ -421,7 +426,8 @@ public class MainActivity extends AppCompatActivity @Override protected void onStart() { super.onStart(); - bindService(new Intent(this, BitmessageService.class), connection, Context.BIND_AUTO_CREATE); + bindService(new Intent(this, BitmessageService.class), connection, Context + .BIND_AUTO_CREATE); } @Override @@ -463,8 +469,10 @@ public class MainActivity extends AppCompatActivity .withEmail(identity.getAddress()) .withTag(identity); if (accountHeader.getProfiles() != null) { - //we know that there are 2 setting elements. set the new profile above them ;) - accountHeader.addProfile(newProfile, accountHeader.getProfiles().size() - 2); + //we know that there are 2 setting elements. set the new profile + // above them ;) + accountHeader.addProfile(newProfile, accountHeader.getProfiles().size + () - 2); } else { accountHeader.addProfiles(newProfile); } diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java index 732ee2e..138ab77 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java @@ -43,7 +43,6 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_ADDRESS; import static ch.dissem.apps.abit.service.BitmessageService.MSG_ADD_CONTACT; import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE; -import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE_AND_ADD_CONTACT; public class OpenBitmessageLinkActivity extends AppCompatActivity { private static final Logger LOG = LoggerFactory.getLogger(OpenBitmessageLinkActivity.class); @@ -106,7 +105,7 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { final int what; if (subscribe.isChecked()) - what = MSG_SUBSCRIBE_AND_ADD_CONTACT; + what = MSG_SUBSCRIBE; else what = MSG_ADD_CONTACT; @@ -155,7 +154,8 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - bindService(new Intent(this, BitmessageService.class), connection, Context.BIND_AUTO_CREATE); + bindService(new Intent(this, BitmessageService.class), connection, Context + .BIND_AUTO_CREATE); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java index 670e299..f7f7acb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java @@ -5,7 +5,7 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; /** - * Created by chris on 14.07.15. + * @author Christian Basler */ public class SettingsActivity extends AppCompatActivity { @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index ffba5a9..75bd0bc 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -1,13 +1,22 @@ package ch.dissem.apps.abit; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +import ch.dissem.apps.abit.synchronization.SyncAdapter; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; /** - * Created by chris on 14.07.15. + * @author Christian Basler */ -public class SettingsFragment extends PreferenceFragment { +public class SettingsFragment + extends PreferenceFragment + implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -15,4 +24,32 @@ public class SettingsFragment extends PreferenceFragment { // Load the preferences from an XML resource addPreferencesFromResource(R.xml.preferences); } + + @Override + public void onAttach(Context ctx) { + super.onAttach(ctx); + PreferenceManager.getDefaultSharedPreferences(ctx) + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + switch (key) { + case PREFERENCE_TRUSTED_NODE: + String node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null); + if (node != null) { + SyncAdapter.startSync(getActivity()); + } else { + SyncAdapter.stopSync(getActivity()); + } + break; + case PREFERENCE_SERVER_POW: + if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) { + SyncAdapter.startPowSync(getActivity()); + } else { + SyncAdapter.stopPowSync(getActivity()); + } + break; + } + } } \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java index 4da7c4e..b84aa54 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java @@ -16,6 +16,7 @@ package ch.dissem.apps.abit; +import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; @@ -25,6 +26,7 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; +import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -100,12 +102,18 @@ public class SubscriptionListFragment extends AbstractItemListFragment cryptoMsg = new CryptoCustomMessage<> + (request); + cryptoMsg.signAndEncrypt( + identity, + security().createPublicKey(identity.getPublicDecryptionKey()) + ); + context.getNetworkHandler().send( + Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), + cryptoMsg); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + }); + } + + @Override + public void setContext(InternalContext context) { + this.context = context; + } +} 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 9d490df..abf123a 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 @@ -19,6 +19,7 @@ package ch.dissem.apps.abit.repository; 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; @@ -27,6 +28,7 @@ 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; @@ -118,27 +120,30 @@ public class AndroidAddressRepository implements AddressRepository { COLUMN_SUBSCRIBED }; + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + where, + null, null, null, null + ); 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)); + 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); + 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); } @@ -153,8 +158,9 @@ public class AndroidAddressRepository implements AddressRepository { } } catch (IOException e) { LOG.error(e.getMessage(), e); + } finally { + c.close(); } - return result; } @@ -173,9 +179,14 @@ public class AndroidAddressRepository implements AddressRepository { 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; + Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address + .getAddress() + "'", null); + try { + cursor.moveToFirst(); + return cursor.getInt(0) > 0; + } finally { + cursor.close(); + } } private void update(BitmessageAddress address) throws IOException { @@ -194,7 +205,8 @@ public class AndroidAddressRepository implements AddressRepository { 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); + int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + + "'", null); 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 5e5f1c5..56ece70 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 @@ -74,15 +74,20 @@ public class AndroidInventory implements Inventory { SQLiteDatabase db = sql.getReadableDatabase(); Cursor c = db.query( TABLE_NAME, projection, - (includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join(streams) + ")", + (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(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); + result.add(new InventoryVector(blob)); + c.moveToNext(); + } + } finally { + c.close(); } return result; } @@ -108,15 +113,19 @@ public class AndroidInventory implements Inventory { "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; - } + try { + 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); + 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); + } finally { + c.close(); + } } @Override @@ -144,13 +153,18 @@ public class AndroidInventory implements Inventory { 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(); + try { + c.moveToFirst(); + 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(); + } + } finally { + c.close(); } return result; } @@ -187,7 +201,11 @@ public class AndroidInventory implements Inventory { "hash = X'" + object.getInventoryVector() + "'", null, null, null, null ); - return c.getCount() > 0; + try { + return c.getCount() > 0; + } finally { + c.close(); + } } @Override 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 eba1b0d..f33c91b 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 @@ -30,6 +30,7 @@ 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 ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +59,7 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont 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 COLUMN_INITIAL_HASH = "initial_hash"; private static final String JOIN_TABLE_NAME = "Message_Label"; private static final String JT_COLUMN_MESSAGE = "message_id"; @@ -112,10 +114,14 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont null, null, null, LBL_COLUMN_ORDER ); - c.moveToFirst(); - while (!c.isAfterLast()) { - result.add(getLabel(c)); - c.moveToNext(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + result.add(getLabel(c)); + c.moveToNext(); + } + } finally { + c.close(); } return result; } @@ -124,27 +130,31 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont 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.broadcasts); - break; - default: - text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); + if (type == null) { + text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); + } else { + 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.broadcasts); + break; + default: + text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); + } } Label label = new Label( text, @@ -158,7 +168,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont public int countUnread(Label label) { 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=" + label.getId() + + ") AND "; } else { where = ""; } @@ -169,13 +180,32 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))", null, null, null, null ); - return c.getColumnCount(); + try { + return c.getColumnCount(); + } finally { + c.close(); + } + } + + @Override + public Plaintext getMessage(byte[] initialHash) { + List results = find("initial_hash=X'" + Strings.hex(initialHash) + "'"); + switch (results.size()) { + case 0: + return null; + case 1: + return results.get(0); + default: + throw new RuntimeException("This shouldn't happen, found " + results.size() + + " messages, one or none was expected"); + } } @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() + ")"); + 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)"); } @@ -183,7 +213,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont @Override public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) { - return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'"); + return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + + "'"); } @Override @@ -213,34 +244,41 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont COLUMN_STATUS }; + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + where, + null, null, null, + COLUMN_RECEIVED + " DESC" + ); try { - SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, - COLUMN_RECEIVED + " DESC" - ); 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)); + 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.from(bmc.getAddressRepository().getAddress(c.getString(c.getColumnIndex + (COLUMN_SENDER)))); + builder.to(bmc.getAddressRepository().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.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); + } finally { + c.close(); } return result; } @@ -257,12 +295,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = bmc.getAddressRepo().getAddress(message.getFrom().getAddress()); + BitmessageAddress savedAddress = bmc.getAddressRepository().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()); + bmc.getAddressRepository().save(message.getFrom()); } } @@ -295,7 +334,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont 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_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()); @@ -303,13 +343,15 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont 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()); 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_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()); @@ -317,6 +359,7 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont 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()); db.update(TABLE_NAME, values, "id = " + message.getId(), null); } 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 new file mode 100644 index 0000000..2273df0 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -0,0 +1,133 @@ +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.Strings; + +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { + private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class); + + private static final String TABLE_NAME = "POW"; + private static final String COLUMN_INITIAL_HASH = "initial_hash"; + private static final String COLUMN_DATA = "data"; + private static final String COLUMN_VERSION = "version"; + private static final String COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"; + private static final String COLUMN_EXTRA_BYTES = "extra_bytes"; + + private final SqlHelper sql; + + public AndroidProofOfWorkRepository(SqlHelper sql) { + this.sql = sql; + } + + @Override + public Item getItem(byte[] initialHash) { + // 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 + }; + + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + "initial_hash = X'" + Strings.hex(initialHash) + "'", + null, null, null, null + ); + try { + c.moveToFirst(); + if (!c.isAfterLast()) { + int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); + 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)) + ); + } + } finally { + c.close(); + } + throw new RuntimeException("Object requested that we don't have. Initial hash: " + + Strings.hex(initialHash)); + } + + @Override + public List<byte[]> getItems() { + // Define a projection that specifies which columns from the database + // you will actually use after this query. + String[] projection = { + COLUMN_INITIAL_HASH + }; + + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + null, null, null, null, null + ); + List<byte[]> result = new LinkedList<>(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)); + result.add(initialHash); + c.moveToNext(); + } + } finally { + c.close(); + } + return result; + } + + @Override + public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + try { + SQLiteDatabase db = sql.getWritableDatabase(); + // Create a new map of values, where column names are the keys + ContentValues values = new ContentValues(); + values.put(COLUMN_INITIAL_HASH, security().getInitialHash(object)); + values.put(COLUMN_DATA, Encode.bytes(object)); + values.put(COLUMN_VERSION, object.getVersion()); + values.put(COLUMN_NONCE_TRIALS_PER_BYTE, nonceTrialsPerByte); + values.put(COLUMN_EXTRA_BYTES, extraBytes); + + db.insertOrThrow(TABLE_NAME, null, values); + } catch (SQLiteConstraintException e) { + LOG.trace(e.getMessage(), e); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + } + + @Override + public void removeObject(byte[] initialHash) { + SQLiteDatabase db = sql.getWritableDatabase(); + db.delete(TABLE_NAME, + "initial_hash = X'" + Strings.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 f79bbb2..9b5083a 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 @@ -26,7 +26,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 = 1; + public static final int DATABASE_VERSION = 2; public static final String DATABASE_NAME = "jabit.db"; protected final Context ctx; @@ -38,7 +38,7 @@ public class SqlHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { - onUpgrade(db, 0, 1); + onUpgrade(db, 0, 2); } @Override @@ -48,6 +48,9 @@ public class SqlHelper extends SQLiteOpenHelper { executeMigration(db, "V1.0__Create_table_inventory"); executeMigration(db, "V1.1__Create_table_address"); executeMigration(db, "V1.2__Create_table_message"); + case 1: + // executeMigration(db, "V2.0__Update_table_message"); + executeMigration(db, "V2.1__Create_table_POW"); default: // 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/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 55f54d5..d2184fd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -34,7 +34,6 @@ public class BitmessageService extends Service { public static final int MSG_CREATE_IDENTITY = 10; public static final int MSG_SUBSCRIBE = 20; public static final int MSG_ADD_CONTACT = 21; - public static final int MSG_SUBSCRIBE_AND_ADD_CONTACT = 23; public static final int MSG_SEND_MESSAGE = 30; public static final int MSG_SEND_BROADCAST = 31; public static final int MSG_START_NODE = 100; @@ -122,6 +121,13 @@ public class BitmessageService extends Service { } break; } + case MSG_ADD_CONTACT: { + Serializable data = msg.getData().getSerializable(DATA_FIELD_ADDRESS); + if (data instanceof BitmessageAddress) { + bmc.addContact((BitmessageAddress) data); + } + break; + } case MSG_SEND_MESSAGE: { Serializable identity = msg.getData().getSerializable(DATA_FIELD_IDENTITY); Serializable address = msg.getData().getSerializable(DATA_FIELD_ADDRESS); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index 7d86ef8..7cb7087 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -73,9 +73,9 @@ public class ProofOfWorkService extends Service { service.startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); engine.calculateNonce(initialHash, target, new ProofOfWorkEngine.Callback() { @Override - public void onNonceCalculated(byte[] nonce) { + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { try { - callback.onNonceCalculated(nonce); + callback.onNonceCalculated(initialHash, nonce); } finally { service.stopForeground(true); service.stopSelf(); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java index 16d2aaa..eaffbdf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java @@ -53,8 +53,8 @@ public class ServicePowEngine implements ProofOfWorkEngine, ProofOfWorkEngine.Ca } @Override - public void onNonceCalculated(byte[] bytes) { - callback.onNonceCalculated(bytes); + public void onNonceCalculated(byte[] initialHash, byte[] bytes) { + callback.onNonceCalculated(initialHash, bytes); ctx.unbindService(connection); } 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 943c3e5..51d9311 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 @@ -2,18 +2,27 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import java.util.List; + import ch.dissem.apps.abit.adapter.AndroidSecurity; +import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine; import ch.dissem.apps.abit.listener.MessageListener; +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.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.ports.AddressRepository; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.security.sc.SpongySecurity; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; + +import static ch.dissem.bitmessage.utils.UnixTime.DAY; /** * Provides singleton objects across the application. @@ -22,6 +31,8 @@ public class Singleton { public static final Object lock = new Object(); private static BitmessageContext bitmessageContext; private static MessageListener messageListener; + private static BitmessageAddress identity; + private static AndroidProofOfWorkRepository powRepo; public static BitmessageContext getBitmessageContext(Context context) { if (bitmessageContext == null) { @@ -29,15 +40,23 @@ public class Singleton { if (bitmessageContext == null) { final Context ctx = context.getApplicationContext(); SqlHelper sqlHelper = new SqlHelper(ctx); + powRepo = new AndroidProofOfWorkRepository(sqlHelper); bitmessageContext = new BitmessageContext.Builder() - .proofOfWorkEngine(new ServicePowEngine(ctx)) - .security(new AndroidSecurity(ctx)) + .proofOfWorkEngine(new SwitchingProofOfWorkEngine( + ctx, Constants.PREFERENCE_SERVER_POW, + new ServerPowEngine(ctx), + new ServicePowEngine(ctx) + )) + .security(new AndroidSecurity()) .nodeRegistry(new MemoryNodeRegistry()) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) + .powRepo(powRepo) .networkHandler(new DefaultNetworkHandler()) .listener(getMessageListener(ctx)) + .doNotSendPubkeyOnIdentityCreation() + .pubkeyTTL(2 * DAY) .build(); } } @@ -63,4 +82,24 @@ public class Singleton { public static AddressRepository getAddressRepository(Context ctx) { return getBitmessageContext(ctx).addresses(); } + + public static ProofOfWorkRepository getProofOfWorkRepository(Context ctx) { + if (powRepo == null) getBitmessageContext(ctx); + return powRepo; + } + + public static BitmessageAddress getIdentity(Context ctx) { + if (identity == null) { + synchronized (Singleton.class) { + if (identity == null) { + List<BitmessageAddress> identities = getBitmessageContext(ctx).addresses() + .getIdentities(); + if (identities.size() > 0) { + identity = identities.get(0); + } + } + } + } + return identity; + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java index 6d4eec5..009b35d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java @@ -12,8 +12,8 @@ import android.os.Bundle; * of its methods */ public class Authenticator extends AbstractAccountAuthenticator { - public static final String ACCOUNT_NAME = "Bitmessage"; - public static final String ACCOUNT_TYPE = "ch.dissem.bitmessage"; + public static final Account ACCOUNT_SYNC = new Account("Bitmessage", "ch.dissem.bitmessage"); + public static final Account ACCOUNT_POW = new Account("Proof of Work ", "ch.dissem.bitmessage"); // Simple constructor public Authenticator(Context context) { diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index c1c55c5..682e165 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -1,27 +1,34 @@ package ch.dissem.apps.abit.synchronization; import android.accounts.Account; +import android.accounts.AccountManager; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; +import android.content.ContentResolver; import android.content.Context; -import android.content.SharedPreferences; import android.content.SyncResult; import android.os.Bundle; -import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.util.List; -import ch.dissem.apps.abit.R; -import ch.dissem.apps.abit.notification.ErrorNotification; import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.extensions.CryptoCustomMessage; +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; +import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_POW; +import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_SYNC; +import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE; +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * Sync Adapter to synchronize with the Bitmessage network - fetches @@ -30,6 +37,8 @@ import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; public class SyncAdapter extends AbstractThreadedSyncAdapter { private final static Logger LOG = LoggerFactory.getLogger(SyncAdapter.class); + private static final long SYNC_FREQUENCY = 15 * 60; // seconds + private final BitmessageContext bmc; /** @@ -41,7 +50,17 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } @Override - public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + if (account.equals(Authenticator.ACCOUNT_SYNC)) + syncData(); + else if (account.equals(Authenticator.ACCOUNT_POW)) + syncPOW(); + else + throw new RuntimeException("Unknown " + account); + } + + private void syncData() { // If the Bitmessage context acts as a full node, synchronization isn't necessary if (bmc.isRunning()) { LOG.info("Synchronization skipped, Abit is acting as a full node"); @@ -49,40 +68,103 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } LOG.info("Synchronizing Bitmessage"); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - - String trustedNode = preferences.getString(PREFERENCE_TRUSTED_NODE, null); - if (trustedNode == null) return; - trustedNode = trustedNode.trim(); - if (trustedNode.isEmpty()) return; - - int port; - if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { - int index = trustedNode.lastIndexOf(':'); - String portString = trustedNode.substring(index + 1); - trustedNode = trustedNode.substring(0, index); - try { - port = Integer.parseInt(portString); - } catch (NumberFormatException e) { - new ErrorNotification(getContext()) - .setError(R.string.error_invalid_sync_port, portString) - .show(); - return; - } - } else { - port = 8444; - } - long timeoutInSeconds = Long.parseLong(preferences.getString(PREFERENCE_SYNC_TIMEOUT, "120")); try { LOG.info("Synchronization started"); - bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); + bmc.synchronize( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + Preferences.getTimeoutInSeconds(getContext()), + true); LOG.info("Synchronization finished"); - } catch (UnknownHostException e) { - new ErrorNotification(getContext()) - .setError(R.string.error_invalid_sync_host) - .show(); } catch (RuntimeException e) { LOG.error(e.getMessage(), e); } } + + private void syncPOW() { + // If the Bitmessage context acts as a full node, synchronization isn't necessary + LOG.info("Looking for completed POW"); + + try { + BitmessageAddress identity = Singleton.getIdentity(getContext()); + byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); + byte[] signingKey = security().createPublicKey(identity.getPublicDecryptionKey()); + ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); + ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); + List<byte[]> items = powRepo.getItems(); + for (byte[] initialHash : items) { + ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); + byte[] target = security().getProofOfWorkTarget(item.object, item + .nonceTrialsPerByte, item.extraBytes); + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( + new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); + cryptoMsg.signAndEncrypt(identity, signingKey); + CustomMessage response = bmc.send( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + cryptoMsg + ); + if (response.isError()) { + LOG.error("Server responded with error: " + new String(response.getData(), + "UTF-8")); + } else { + ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read( + response, reader).decrypt(privateKey); + if (decryptedResponse.getRequest() == COMPLETE) { + bmc.internals().getProofOfWorkService().onNonceCalculated( + initialHash, decryptedResponse.getData()); + } + } + } + if (items.size() == 0) { + stopPowSync(getContext()); + } + LOG.info("Synchronization finished"); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + public static void startSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_SYNC); + + // Recommend a schedule for automatic synchronization. The system may modify this based + // on other scheduled syncs and network utilization. + ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); + } + + public static void stopSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_SYNC); + + ContentResolver.removePeriodicSync(account, AUTHORITY, new Bundle()); + } + + + public static void startPowSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_POW); + + // Recommend a schedule for automatic synchronization. The system may modify this based + // on other scheduled syncs and network utilization. + ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); + } + + public static void stopPowSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_POW); + + ContentResolver.removePeriodicSync(account, AUTHORITY, new Bundle()); + } + + private static Account addAccount(Context ctx, Account account) { + if (AccountManager.get(ctx).addAccountExplicitly(account, null, null)) { + // Inform the system that this account supports sync + ContentResolver.setIsSyncable(account, AUTHORITY, 1); + // Inform the system that this account is eligible for auto sync when the network is up + ContentResolver.setSyncAutomatically(account, AUTHORITY, true); + } + return account; + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java new file mode 100644 index 0000000..20c5862 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -0,0 +1,90 @@ +package ch.dissem.apps.abit.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.notification.ErrorNotification; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; + +/** + * Created by chrig on 01.12.2015. + */ +public class Preferences { + private static Logger LOG = LoggerFactory.getLogger(Preferences.class); + + public static boolean useTrustedNode(Context ctx) { + String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); + return trustedNode == null || trustedNode.trim().isEmpty(); + } + + /** + * Warning, this method might do a network call and therefore can't be called from + * the UI thread. + */ + public static InetAddress getTrustedNode(Context ctx) { + String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); + if (trustedNode == null) return null; + trustedNode = trustedNode.trim(); + if (trustedNode.isEmpty()) return null; + + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { + int index = trustedNode.lastIndexOf(':'); + trustedNode = trustedNode.substring(0, index); + } + try { + return InetAddress.getByName(trustedNode); + } catch (UnknownHostException e) { + new ErrorNotification(ctx) + .setError(R.string.error_invalid_sync_host) + .show(); + LOG.error(e.getMessage(), e); + return null; + } + } + + public static int getTrustedNodePort(Context ctx) { + String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); + if (trustedNode == null) return 8444; + trustedNode = trustedNode.trim(); + + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { + int index = trustedNode.lastIndexOf(':'); + String portString = trustedNode.substring(index + 1); + try { + return Integer.parseInt(portString); + } catch (NumberFormatException e) { + new ErrorNotification(ctx) + .setError(R.string.error_invalid_sync_port, portString) + .show(); + } + } + return 8444; + } + + public static long getTimeoutInSeconds(Context ctx) { + String preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT); + return preference == null ? 120 : Long.parseLong(preference); + } + + public static boolean isServerPOW(Context ctx) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + return preferences.getBoolean(PREFERENCE_SERVER_POW, false); + } + + private static String getPreference(Context ctx, String name) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + + return preferences.getString(name, null); + } +} diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 05b6937..50fea51 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -27,7 +27,7 @@ <SwitchPreference android:defaultValue="false" android:key="server_pow" - android:dependency="@string/trusted_node" + android:dependency="trusted_node" android:title="@string/server_pow" android:summary="@string/server_pow_summary" /> diff --git a/build.gradle b/build.gradle index 7acef02..f0f1e2b 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.5.0-beta1' + classpath 'com.android.tools.build:gradle:1.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files