diff --git a/app/build.gradle b/app/build.gradle index 0f7de7f..0bd40c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply plugin: 'idea' ext { @@ -41,6 +42,7 @@ ext.jabitVersion = 'feature-exports-SNAPSHOT' ext.supportVersion = '25.3.1' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" compile "com.android.support:appcompat-v7:$supportVersion" compile "com.android.support:preference-v7:$supportVersion" 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 deleted file mode 100644 index 2722ca1..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java +++ /dev/null @@ -1,195 +0,0 @@ -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 android.support.annotation.NonNull; - -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)}); - } - - @Override - public void clear() { - SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, null, null); - } - - 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; - } - } - - @NonNull - @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)}, - 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/AndroidNodeRegistry.kt b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.kt new file mode 100644 index 0000000..dbaa27f --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.kt @@ -0,0 +1,172 @@ +package ch.dissem.apps.abit.repository + +import android.content.ContentValues +import android.database.sqlite.SQLiteConstraintException +import android.database.sqlite.SQLiteDoneException +import android.database.sqlite.SQLiteStatement +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress +import ch.dissem.bitmessage.exception.ApplicationException +import ch.dissem.bitmessage.ports.NodeRegistry +import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes +import ch.dissem.bitmessage.utils.Collections +import ch.dissem.bitmessage.utils.SqlStrings +import ch.dissem.bitmessage.utils.Strings.hex +import ch.dissem.bitmessage.utils.UnixTime.DAY +import ch.dissem.bitmessage.utils.UnixTime.MINUTE +import ch.dissem.bitmessage.utils.UnixTime.now +import org.slf4j.LoggerFactory +import java.util.* +import kotlin.concurrent.getOrSet + +const val MAX_ENTRY_AGE = 7 * DAY + +/** + * @author Christian Basler + */ +class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry { + + private val loadExistingStatement = ThreadLocal() + private var stableNodes: Map> = emptyMap() + get() { + if (field.isEmpty()) + field = loadStableNodes() + return field + } + + init { + cleanUp() + } + + private fun cleanUp() { + sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString())) + } + + override fun clear() { + sql.writableDatabase.delete(TABLE_NAME, null, null) + } + + private fun loadExistingTime(node: NetworkAddress): Long? { + val statement: SQLiteStatement = loadExistingStatement.getOrSet { + sql.writableDatabase.compileStatement( + "SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?" + ) + } + statement.bindLong(1, node.stream) + statement.bindBlob(2, node.IPv6) + statement.bindLong(3, node.port.toLong()) + try { + return statement.simpleQueryForLong() + } catch (e: SQLiteDoneException) { + return null + } + } + + override fun getKnownAddresses(limit: Int, vararg streams: Long): List { + val projection = arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME) + + val result = LinkedList() + try { + sql.readableDatabase.query( + TABLE_NAME, projection, + "stream IN (?)", + arrayOf(SqlStrings.join(*streams)), null, null, + "time DESC", + limit.toString() + ).use { c -> + while (c.moveToNext()) { + result.add(NetworkAddress( + time = c.getLong(c.getColumnIndex(COLUMN_TIME)), + stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)), + services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)), + IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)), + port = c.getInt(c.getColumnIndex(COLUMN_PORT)) + )) + } + } + } catch (e: Exception) { + LOG.error(e.message, e) + throw ApplicationException(e) + } + + if (result.isEmpty()) { + streams + .asSequence() + .mapNotNull { stableNodes[it] } + .filterNot { it.isEmpty() } + .mapTo(result) { Collections.selectRandom(it) } + } + return result + } + + override fun offerAddresses(nodes: List) { + val db = sql.writableDatabase + db.beginTransaction() + try { + cleanUp() + nodes + .filter { + // Don't accept nodes from the future, it might be a trap + it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE + } + .forEach { node -> + synchronized(this) { + val existing = loadExistingTime(node) + if (existing == null) { + insert(node) + } else if (node.time > existing) { + update(node) + } + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun insert(node: NetworkAddress) { + try { + // Create a new map of values, where column names are the keys + val values = ContentValues() + values.put(COLUMN_STREAM, node.stream) + values.put(COLUMN_ADDRESS, node.IPv6) + values.put(COLUMN_PORT, node.port) + values.put(COLUMN_SERVICES, node.services) + values.put(COLUMN_TIME, node.time) + + sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values) + } catch (e: SQLiteConstraintException) { + LOG.trace(e.message, e) + } + } + + private fun update(node: NetworkAddress) { + try { + // Create a new map of values, where column names are the keys + val values = ContentValues() + values.put(COLUMN_SERVICES, node.services) + values.put(COLUMN_TIME, node.time) + + sql.writableDatabase.update( + TABLE_NAME, + values, + "stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}", + null + ) + } catch (e: SQLiteConstraintException) { + LOG.trace(e.message, e) + } + } + + companion object { + @JvmStatic + private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java) + + private const val TABLE_NAME = "Node" + private const val COLUMN_STREAM = "stream" + private const val COLUMN_ADDRESS = "address" + private const val COLUMN_PORT = "port" + private const val COLUMN_SERVICES = "services" + private const val COLUMN_TIME = "time" + } +} 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 deleted file mode 100644 index 0b33907..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.service; - -import android.app.Service; -import android.content.Intent; -import android.os.Handler; -import android.os.IBinder; -import android.support.annotation.Nullable; - -import ch.dissem.apps.abit.notification.NetworkNotification; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.utils.Property; - -import static ch.dissem.apps.abit.notification.NetworkNotification.NETWORK_NOTIFICATION_ID; - -/** - * Define a Service that returns an IBinder for the - * sync adapter class, allowing the sync adapter framework to call - * onPerformSync(). - */ -public class BitmessageService extends Service { - private static BitmessageContext bmc = null; - private static volatile boolean running = false; - - private NetworkNotification notification = null; - - private final Handler cleanupHandler = new Handler(); - private final Runnable cleanupTask = new Runnable() { - @Override - public void run() { - bmc.cleanup(); - if (isRunning()) { - cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L); - } - } - }; - - public static boolean isRunning() { - return running && bmc.isRunning(); - } - - @Override - public void onCreate() { - if (bmc == null) { - bmc = Singleton.getBitmessageContext(this); - } - notification = new NetworkNotification(this); - running = false; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (!isRunning()) { - running = true; - notification.connecting(); - startForeground(NETWORK_NOTIFICATION_ID, notification.getNotification()); - if (!bmc.isRunning()) { - bmc.startup(); - } - notification.show(); - cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L); - } - return Service.START_STICKY; - } - - @Override - public void onDestroy() { - if (bmc.isRunning()) { - bmc.shutdown(); - } - running = false; - notification.showShutdown(); - cleanupHandler.removeCallbacks(cleanupTask); - bmc.cleanup(); - stopSelf(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - public static Property getStatus() { - if (bmc != null) { - return bmc.status(); - } else { - return new Property("bitmessage context"); - } - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.kt b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.kt new file mode 100644 index 0000000..5bd0339 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.service + +import android.app.Service +import android.content.Intent +import android.os.Handler +import ch.dissem.apps.abit.notification.NetworkNotification +import ch.dissem.apps.abit.notification.NetworkNotification.NETWORK_NOTIFICATION_ID +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.utils.Property + +/** + * Define a Service that returns an IBinder for the + * sync adapter class, allowing the sync adapter framework to call + * onPerformSync(). + */ +class BitmessageService : Service() { + + private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) } + private lateinit var notification: NetworkNotification + + private val cleanupHandler = Handler() + private val cleanupTask = object : Runnable { + override fun run() { + bmc.cleanup() + if (isRunning) { + cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day + } + } + } + + override fun onCreate() { + notification = NetworkNotification(this) + running = false + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if (!isRunning) { + running = true + notification.connecting() + startForeground(NETWORK_NOTIFICATION_ID, notification.notification) + if (!bmc.isRunning()) { + bmc.startup() + } + notification.show() + cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L) + } + return Service.START_STICKY + } + + override fun onDestroy() { + if (bmc.isRunning()) { + bmc.shutdown() + } + running = false + notification.showShutdown() + cleanupHandler.removeCallbacks(cleanupTask) + bmc.cleanup() + stopSelf() + } + + override fun onBind(intent: Intent) = null + + companion object { + @Volatile private var running = false + + @JvmStatic + val isRunning: Boolean + get() = running && Singleton.bitmessageContext?.isRunning() ?: false + + @JvmStatic + val status: Property + get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context") + } +} 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 deleted file mode 100644 index 64c6ff7..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.service; - -import android.content.Context; -import android.os.AsyncTask; -import android.widget.Toast; - -import java.util.List; - -import ch.dissem.apps.abit.MainActivity; -import ch.dissem.apps.abit.R; -import ch.dissem.apps.abit.adapter.AndroidCryptography; -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.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.entity.payload.Pubkey; -import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; -import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.ports.ProofOfWorkRepository; -import ch.dissem.bitmessage.utils.ConversationService; -import ch.dissem.bitmessage.utils.TTL; - -import static ch.dissem.bitmessage.utils.UnixTime.DAY; - -/** - * Provides singleton objects across the application. - */ -public class Singleton { - private static BitmessageContext bitmessageContext; - private static ConversationService conversationService; - private static MessageListener messageListener; - private static BitmessageAddress identity; - private static AndroidProofOfWorkRepository powRepo; - private static boolean creatingIdentity; - - public static BitmessageContext getBitmessageContext(Context context) { - if (bitmessageContext == null) { - synchronized (Singleton.class) { - if (bitmessageContext == null) { - 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, - new ServerPowEngine(ctx), - new ServicePowEngine(ctx) - )) - .cryptography(new AndroidCryptography()) - .nodeRegistry(new AndroidNodeRegistry(sqlHelper)) - .inventory(new AndroidInventory(sqlHelper)) - .addressRepo(new AndroidAddressRepository(sqlHelper)) - .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) - .powRepo(powRepo) - .networkHandler(new NioNetworkHandler()) - .listener(getMessageListener(ctx)) - .doNotSendPubkeyOnIdentityCreation() - .build(); - } - } - } - return bitmessageContext; - } - - public static MessageListener getMessageListener(Context ctx) { - if (messageListener == null) { - synchronized (Singleton.class) { - if (messageListener == null) { - messageListener = new MessageListener(ctx); - } - } - } - return messageListener; - } - - public static AndroidMessageRepository getMessageRepository(Context ctx) { - return (AndroidMessageRepository) getBitmessageContext(ctx).messages(); - } - - public static AndroidAddressRepository getAddressRepository(Context ctx) { - return (AndroidAddressRepository) getBitmessageContext(ctx).addresses(); - } - - public static ProofOfWorkRepository getProofOfWorkRepository(Context ctx) { - if (powRepo == null) getBitmessageContext(ctx); - return powRepo; - } - - public static BitmessageAddress getIdentity(final Context ctx) { - if (identity == null) { - final BitmessageContext bmc = getBitmessageContext(ctx); - synchronized (Singleton.class) { - if (identity == null) { - List identities = bmc.addresses() - .getIdentities(); - if (identities.size() > 0) { - identity = identities.get(0); - } else { - if (!creatingIdentity) { - creatingIdentity = true; - new AsyncTask() { - @Override - protected BitmessageAddress doInBackground(Void... args) { - BitmessageAddress identity = bmc.createIdentity(false, - Pubkey.Feature.DOES_ACK); - identity.setAlias( - ctx.getString(R.string.alias_default_identity) - ); - bmc.addresses().save(identity); - return identity; - } - - @Override - protected void onPostExecute(BitmessageAddress identity) { - Singleton.identity = identity; - Toast.makeText(ctx, - R.string.toast_identity_created, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.addIdentityEntry(identity); - } - } - }.execute(); - } - return null; - } - } - } - } - return identity; - } - - public static void setIdentity(BitmessageAddress identity) { - if (identity.getPrivateKey() == null) - throw new IllegalArgumentException("Identity expected, but no private key available"); - Singleton.identity = identity; - } - - public static ConversationService getConversationService(Context ctx) { - if (conversationService == null) { - final BitmessageContext bmc = getBitmessageContext(ctx); - synchronized (Singleton.class) { - if (conversationService == null) { - conversationService = new ConversationService(bmc.messages()); - } - } - } - return conversationService; - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.kt b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.kt new file mode 100644 index 0000000..b2443ac --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.service + +import android.content.Context +import android.os.AsyncTask +import android.widget.Toast +import ch.dissem.apps.abit.MainActivity +import ch.dissem.apps.abit.R +import ch.dissem.apps.abit.adapter.AndroidCryptography +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.* +import ch.dissem.apps.abit.util.Constants +import ch.dissem.bitmessage.BitmessageContext +import ch.dissem.bitmessage.entity.BitmessageAddress +import ch.dissem.bitmessage.entity.payload.Pubkey +import ch.dissem.bitmessage.networking.nio.NioNetworkHandler +import ch.dissem.bitmessage.ports.ProofOfWorkRepository +import ch.dissem.bitmessage.utils.ConversationService +import ch.dissem.bitmessage.utils.TTL +import ch.dissem.bitmessage.utils.UnixTime.DAY + +/** + * Provides singleton objects across the application. + */ +object Singleton { + var bitmessageContext: BitmessageContext? = null + private set + private var conversationService: ConversationService? = null + private var messageListener: MessageListener? = null + private var identity: BitmessageAddress? = null + private var powRepo: AndroidProofOfWorkRepository? = null + private var creatingIdentity: Boolean = false + + @JvmStatic + fun getBitmessageContext(context: Context): BitmessageContext { + if (bitmessageContext == null) { + synchronized(Singleton::class.java) { + if (bitmessageContext == null) { + val ctx = context.applicationContext + val sqlHelper = SqlHelper(ctx) + powRepo = AndroidProofOfWorkRepository(sqlHelper) + TTL.pubkey = 2 * DAY + bitmessageContext = BitmessageContext.Builder() + .proofOfWorkEngine(SwitchingProofOfWorkEngine( + ctx, Constants.PREFERENCE_SERVER_POW, + ServerPowEngine(ctx), + ServicePowEngine(ctx) + )) + .cryptography(AndroidCryptography()) + .nodeRegistry(AndroidNodeRegistry(sqlHelper)) + .inventory(AndroidInventory(sqlHelper)) + .addressRepo(AndroidAddressRepository(sqlHelper)) + .messageRepo(AndroidMessageRepository(sqlHelper, ctx)) + .powRepo(powRepo!!) + .networkHandler(NioNetworkHandler()) + .listener(getMessageListener(ctx)) + .doNotSendPubkeyOnIdentityCreation() + .build() + } + } + } + return bitmessageContext!! + } + + @JvmStatic + fun getMessageListener(ctx: Context): MessageListener { + if (messageListener == null) { + synchronized(Singleton::class.java) { + if (messageListener == null) { + messageListener = MessageListener(ctx) + } + } + } + return messageListener!! + } + + @JvmStatic + fun getMessageRepository(ctx: Context): AndroidMessageRepository { + return getBitmessageContext(ctx).messages as AndroidMessageRepository + } + + @JvmStatic + fun getAddressRepository(ctx: Context): AndroidAddressRepository { + return getBitmessageContext(ctx).addresses as AndroidAddressRepository + } + + @JvmStatic + fun getProofOfWorkRepository(ctx: Context): ProofOfWorkRepository { + if (powRepo == null) getBitmessageContext(ctx) + return powRepo!! + } + + @JvmStatic + fun getIdentity(ctx: Context): BitmessageAddress? { + if (identity == null) { + val bmc = getBitmessageContext(ctx) + synchronized(Singleton::class) { + if (identity == null) { + val identities = bmc.addresses.getIdentities() + if (identities.isNotEmpty()) { + identity = identities[0] + } else { + if (!creatingIdentity) { + creatingIdentity = true + object : AsyncTask() { + override fun doInBackground(vararg args: Void): BitmessageAddress { + val identity = bmc.createIdentity(false, + Pubkey.Feature.DOES_ACK) + identity.alias = ctx.getString(R.string.alias_default_identity) + bmc.addresses.save(identity) + return identity + } + + override fun onPostExecute(identity: BitmessageAddress) { + Singleton.identity = identity + Toast.makeText(ctx, + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show() + val mainActivity = MainActivity.getInstance() + mainActivity?.addIdentityEntry(identity) + } + }.execute() + } + return null + } + } + } + } + return identity + } + + @JvmStatic + fun setIdentity(identity: BitmessageAddress) { + if (identity.privateKey == null) + throw IllegalArgumentException("Identity expected, but no private key available") + Singleton.identity = identity + } + + @JvmStatic + fun getConversationService(ctx: Context): ConversationService { + if (conversationService == null) { + val bmc = getBitmessageContext(ctx) + synchronized(Singleton::class.java) { + if (conversationService == null) { + conversationService = ConversationService(bmc.messages) + } + } + } + return conversationService!! + } +} 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 deleted file mode 100644 index a9f2dd2..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.synchronization; - -import android.accounts.AbstractAccountAuthenticator; -import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.NetworkErrorException; -import android.content.Context; -import android.os.Bundle; - -/* - * Implement AbstractAccountAuthenticator and stub out all - * of its methods - */ -public class Authenticator extends AbstractAccountAuthenticator { - 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) { - super(context); - } - - // Editing properties is not supported - @Override - public Bundle editProperties( - AccountAuthenticatorResponse r, String s) { - throw new UnsupportedOperationException(); - } - - // Don't add additional accounts - @Override - public Bundle addAccount( - AccountAuthenticatorResponse r, - String s, - String s2, - String[] strings, - Bundle bundle) throws NetworkErrorException { - return null; - } - - // Ignore attempts to confirm credentials - @Override - public Bundle confirmCredentials( - AccountAuthenticatorResponse r, - Account account, - Bundle bundle) throws NetworkErrorException { - return null; - } - - // Getting an authentication token is not supported - @Override - public Bundle getAuthToken( - AccountAuthenticatorResponse r, - Account account, - String s, - Bundle bundle) throws NetworkErrorException { - throw new UnsupportedOperationException(); - } - - // Getting a label for the auth token is not supported - @Override - public String getAuthTokenLabel(String s) { - throw new UnsupportedOperationException(); - } - - // Updating user credentials is not supported - @Override - public Bundle updateCredentials( - AccountAuthenticatorResponse r, - Account account, - String s, Bundle bundle) throws NetworkErrorException { - throw new UnsupportedOperationException(); - } - - // Checking features for the account is not supported - @Override - public Bundle hasFeatures( - AccountAuthenticatorResponse r, - Account account, String[] strings) throws NetworkErrorException { - throw new UnsupportedOperationException(); - } -} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.kt b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.kt new file mode 100644 index 0000000..5a149bb --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.synchronization + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.NetworkErrorException +import android.content.Context +import android.os.Bundle + +/** + * Implement AbstractAccountAuthenticator and stub out all + * of its methods + */ +class Authenticator(context: Context) : AbstractAccountAuthenticator(context) { + + // Editing properties is not supported + override fun editProperties(r: AccountAuthenticatorResponse, s: String) = throw UnsupportedOperationException() + + // Don't add additional accounts + @Throws(NetworkErrorException::class) + override fun addAccount( + r: AccountAuthenticatorResponse, + s: String, + s2: String, + strings: Array, + bundle: Bundle) = null + + // Ignore attempts to confirm credentials + @Throws(NetworkErrorException::class) + override fun confirmCredentials( + r: AccountAuthenticatorResponse, + account: Account, + bundle: Bundle) = null + + // Getting an authentication token is not supported + @Throws(NetworkErrorException::class) + override fun getAuthToken( + r: AccountAuthenticatorResponse, + account: Account, + s: String, + bundle: Bundle) = throw UnsupportedOperationException() + + // Getting a label for the auth token is not supported + override fun getAuthTokenLabel(s: String) = throw UnsupportedOperationException() + + // Updating user credentials is not supported + @Throws(NetworkErrorException::class) + override fun updateCredentials( + r: AccountAuthenticatorResponse, + account: Account, + s: String, bundle: Bundle) = throw UnsupportedOperationException() + + // Checking features for the account is not supported + @Throws(NetworkErrorException::class) + override fun hasFeatures( + r: AccountAuthenticatorResponse, + account: Account, strings: Array) = throw UnsupportedOperationException() + + companion object { + @JvmField val ACCOUNT_SYNC = Account("Bitmessage", "ch.dissem.bitmessage") + @JvmField val ACCOUNT_POW = Account("Proof of Work ", "ch.dissem.bitmessage") + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.kt similarity index 68% rename from app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java rename to app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.kt index 1534e33..5d2418d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.kt @@ -14,34 +14,30 @@ * limitations under the License. */ -package ch.dissem.apps.abit.synchronization; +package ch.dissem.apps.abit.synchronization -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; +import android.app.Service +import android.content.Intent +import android.os.IBinder /** * A bound Service that instantiates the authenticator * when started. */ -public class AuthenticatorService extends Service { +class AuthenticatorService : Service() { /** * Instance field that stores the authenticator object */ - private Authenticator authenticator; + private var authenticator: Authenticator? = null - @Override - public void onCreate() { + override fun onCreate() { // Create a new authenticator object - authenticator = new Authenticator(this); + authenticator = Authenticator(this) } /* * When the system binds to this Service to make the RPC call * return the authenticator's IBinder. */ - @Override - public IBinder onBind(Intent intent) { - return authenticator.getIBinder(); - } -} \ No newline at end of file + override fun onBind(intent: Intent) = authenticator?.iBinder +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java deleted file mode 100644 index 1ac156e..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.synchronization; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.NonNull; - -/* - * Define an implementation of ContentProvider that stubs out - * all methods - */ -public class StubProvider extends ContentProvider { - public static final String AUTHORITY = "ch.dissem.apps.abit.provider"; - - /* - * Always return true, indicating that the - * provider loaded correctly. - */ - @Override - public boolean onCreate() { - return true; - } - - /* - * Return no type for MIME type - */ - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - /* - * query() always returns no results - * - */ - @Override - public Cursor query( - @NonNull Uri uri, - String[] projection, - String selection, - String[] selectionArgs, - String sortOrder) { - return null; - } - - /* - * insert() always returns null (no URI) - */ - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - return null; - } - - /* - * delete() always returns "no rows affected" (0) - */ - @Override - public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { - return 0; - } - - /* - * update() always returns "no rows affected" (0) - */ - public int update( - @NonNull Uri uri, - ContentValues values, - String selection, - String[] selectionArgs) { - return 0; - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.kt b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.kt new file mode 100644 index 0000000..d456928 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.synchronization + +import android.content.ContentProvider +import android.content.ContentValues +import android.net.Uri + +/* + * Define an implementation of ContentProvider that stubs out + * all methods + */ +class StubProvider : ContentProvider() { + + /** + * Always return true, indicating that the + * provider loaded correctly. + */ + override fun onCreate() = true + + /** + * Return no type for MIME type + */ + override fun getType(uri: Uri) = null + + /** + * query() always returns no results + */ + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String?) = null + + /** + * insert() always returns null (no URI) + */ + override fun insert(uri: Uri, values: ContentValues?) = null + + /** + * delete() always returns "no rows affected" (0) + */ + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = 0 + + /** + * update() always returns "no rows affected" (0) + */ + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array?) = 0 + + companion object { + const val AUTHORITY = "ch.dissem.apps.abit.provider" + } +} 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 deleted file mode 100644 index ee42b51..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.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.SyncResult; -import android.os.Bundle; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; - -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.exception.DecryptionFailedException; -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.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.cryptography; - -/** - * Sync Adapter to synchronize with the Bitmessage network - fetches - * new objects and then disconnects. - */ -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; - - /** - * Set up the sync adapter - */ - public SyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); - bmc = Singleton.getBitmessageContext(context); - } - - @Override - public void onPerformSync(Account account, Bundle extras, String authority, - ContentProviderClient provider, SyncResult syncResult) { - try { - if (account.equals(ACCOUNT_SYNC)) { - if (Preferences.isConnectionAllowed(getContext())) { - syncData(); - } - } else if (account.equals(ACCOUNT_POW)) { - syncPOW(); - } else { - syncResult.stats.numAuthExceptions++; - } - } catch (IOException e) { - syncResult.stats.numIoExceptions++; - } catch (DecryptionFailedException e) { - syncResult.stats.numAuthExceptions++; - } - } - - private void syncData() throws IOException { - // 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"); - return; - } - LOG.info("Synchronizing Bitmessage"); - - LOG.info("Synchronization started"); - bmc.synchronize( - Preferences.getTrustedNode(getContext()), - Preferences.getTrustedNodePort(getContext()), - Preferences.getTimeoutInSeconds(getContext()), - true - ); - LOG.info("Synchronization finished"); - } - - private void syncPOW() throws IOException, DecryptionFailedException { - // If the Bitmessage context acts as a full node, synchronization isn't necessary - LOG.info("Looking for completed POW"); - - BitmessageAddress identity = Singleton.getIdentity(getContext()); - @SuppressWarnings("ConstantConditions") - byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); - byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey()); - ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); - ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); - List items = powRepo.getItems(); - for (byte[] initialHash : items) { - ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); - byte[] target = cryptography().getProofOfWorkTarget(item.getObjectMessage(), item.getNonceTrialsPerByte(), item.getExtraBytes()); - CryptoCustomMessage 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"); - } - - 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/synchronization/SyncAdapter.kt b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.kt new file mode 100644 index 0000000..57e77f3 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.synchronization + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.* +import android.os.Bundle +import ch.dissem.apps.abit.service.Singleton +import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW +import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC +import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY +import ch.dissem.apps.abit.util.Preferences +import ch.dissem.bitmessage.exception.DecryptionFailedException +import ch.dissem.bitmessage.extensions.CryptoCustomMessage +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE +import ch.dissem.bitmessage.utils.Singleton.cryptography +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * Sync Adapter to synchronize with the Bitmessage network - fetches + * new objects and then disconnects. + */ +class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedSyncAdapter(context, autoInitialize) { + + private val bmc = Singleton.getBitmessageContext(context) + + override fun onPerformSync( + account: Account, + extras: Bundle, + authority: String, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + if (account == ACCOUNT_SYNC) { + if (Preferences.isConnectionAllowed(context)) { + syncData() + } + } else if (account == ACCOUNT_POW) { + syncPOW() + } else { + syncResult.stats.numAuthExceptions++ + } + } catch (e: IOException) { + syncResult.stats.numIoExceptions++ + } catch (e: DecryptionFailedException) { + syncResult.stats.numAuthExceptions++ + } + + } + + private fun 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") + return + } + val trustedNode = Preferences.getTrustedNode(context) + if (trustedNode == null) { + LOG.info("Trusted node not available, disabling synchronization") + stopSync(context) + return + } + LOG.info("Synchronization started") + bmc.synchronize( + trustedNode, + Preferences.getTrustedNodePort(context), + Preferences.getTimeoutInSeconds(context), + true + ) + LOG.info("Synchronization finished") + } + + private fun syncPOW() { + val identity = Singleton.getIdentity(context) + if (identity == null) { + LOG.info("No identity available - skipping POW synchronization") + return + } + val trustedNode = Preferences.getTrustedNode(context) + if (trustedNode == null) { + LOG.info("Trusted node not available, disabling POW synchronization") + stopPowSync(context) + return + } + // If the Bitmessage context acts as a full node, synchronization isn't necessary + LOG.info("Looking for completed POW") + + val privateKey = identity.privateKey!!.privateEncryptionKey + val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey) + val reader = ProofOfWorkRequest.Reader(identity) + val powRepo = Singleton.getProofOfWorkRepository(context) + val items = powRepo.getItems() + for (initialHash in items) { + val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash) + val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes) + val cryptoMsg = CryptoCustomMessage( + ProofOfWorkRequest(identity, initialHash, CALCULATE, target)) + cryptoMsg.signAndEncrypt(identity, signingKey) + val response = bmc.send( + trustedNode, + Preferences.getTrustedNodePort(context), + cryptoMsg + ) + if (response.isError) { + LOG.error("Server responded with error: ${String(response.getData())}") + } else { + val (_, _, request, data) = CryptoCustomMessage.read(response, reader).decrypt(privateKey) + if (request == COMPLETE) { + bmc.internals.proofOfWorkService.onNonceCalculated(initialHash, data) + } + } + } + if (items.isEmpty()) { + stopPowSync(context) + } + LOG.info("Synchronization finished") + } + + companion object { + private val LOG = LoggerFactory.getLogger(SyncAdapter::class.java) + + private const val SYNC_FREQUENCY = 15 * 60L // seconds + + @JvmStatic + fun startSync(ctx: Context) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + val 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, Bundle(), SYNC_FREQUENCY) + } + + @JvmStatic + fun stopSync(ctx: Context) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + val account = addAccount(ctx, ACCOUNT_SYNC) + + ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle()) + } + + @JvmStatic + fun startPowSync(ctx: Context) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + val 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, Bundle(), SYNC_FREQUENCY) + } + + @JvmStatic + fun stopPowSync(ctx: Context) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + val account = addAccount(ctx, ACCOUNT_POW) + + ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle()) + } + + private fun addAccount(ctx: Context, account: 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/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java deleted file mode 100644 index a11b120..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.synchronization; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -/** - * Define a Service that returns an IBinder for the - * sync adapter class, allowing the sync adapter framework to call - * onPerformSync(). - */ -public class SyncService extends Service { - // Storage for an instance of the sync adapter - private static SyncAdapter syncAdapter = null; - // Object to use as a thread-safe lock - private static final Object syncAdapterLock = new Object(); - - /** - * Instantiate the sync adapter object. - */ - @Override - public void onCreate() { - /* - * Create the sync adapter as a singleton. - * Set the sync adapter as syncable - * Disallow parallel syncs - */ - synchronized (syncAdapterLock) { - if (syncAdapter == null) { - syncAdapter = new SyncAdapter(this, true); - } - } - } - - /** - * Return an object that allows the system to invoke - * the sync adapter. - */ - @Override - public IBinder onBind(Intent intent) { - /* - * Get the object that allows external processes - * to call onPerformSync(). The object is created - * in the base class code when the SyncAdapter - * constructors call super() - */ - return syncAdapter.getSyncAdapterBinder(); - } -} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.kt b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.kt new file mode 100644 index 0000000..515c50c --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.synchronization + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +/** + * Define a Service that returns an IBinder for the + * sync adapter class, allowing the sync adapter framework to call + * onPerformSync(). + */ +class SyncService : Service() { + + /** + * Instantiate the sync adapter object. + */ + override fun onCreate() { + // Create the sync adapter as a singleton. + // Set the sync adapter as syncable + // Disallow parallel syncs + synchronized(syncAdapterLock) { + if (syncAdapter == null) { + syncAdapter = SyncAdapter(this, true) + } + } + } + + /** + * Return an object that allows the system to invoke + * the sync adapter. + */ + override fun onBind(intent: Intent): IBinder? { + // Get the object that allows external processes + // to call onPerformSync(). The object is created + // in the base class code when the SyncAdapter + // constructors call super() + return syncAdapter?.syncAdapterBinder + } + + companion object { + // Storage for an instance of the sync adapter + private var syncAdapter: SyncAdapter? = null + // Object to use as a thread-safe lock + private val syncAdapterLock = Any() + } +} 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 deleted file mode 100644 index ebb2dfb..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import java.io.IOException; -import java.net.InetAddress; - -import ch.dissem.apps.abit.R; -import ch.dissem.apps.abit.listener.WifiReceiver; -import ch.dissem.apps.abit.notification.ErrorNotification; - -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_FULL_NODE; -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.util.Constants.PREFERENCE_WIFI_ONLY; - -/** - * @author Christian Basler - */ -public class Preferences { - 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) throws IOException { - 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); - } - return InetAddress.getByName(trustedNode); - } - - 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); - } - - private static String getPreference(Context ctx, String name) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - - return preferences.getString(name, null); - } - - public static boolean isConnectionAllowed(Context ctx) { - return !isWifiOnly(ctx) || !WifiReceiver.isConnectedToMeteredNetwork(ctx); - } - - public static boolean isWifiOnly(Context ctx) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true); - } - - public static void setWifiOnly(Context ctx, boolean status) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - preferences.edit().putBoolean(PREFERENCE_WIFI_ONLY, status).apply(); - } - - public static boolean isFullNodeActive(Context ctx) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - return preferences.getBoolean(PREFERENCE_FULL_NODE, false); - } - - public static void setFullNodeActive(Context ctx, boolean status) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - preferences.edit().putBoolean(PREFERENCE_FULL_NODE, status).apply(); - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.kt b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.kt new file mode 100644 index 0000000..4f03bfe --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.util + +import android.content.Context +import android.preference.PreferenceManager +import ch.dissem.apps.abit.R +import ch.dissem.apps.abit.listener.WifiReceiver +import ch.dissem.apps.abit.notification.ErrorNotification +import ch.dissem.apps.abit.util.Constants.* +import java.io.IOException +import java.net.InetAddress + +/** + * @author Christian Basler + */ +object Preferences { + @JvmStatic + fun useTrustedNode(ctx: Context): Boolean { + val trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return false + return trustedNode.trim { it <= ' ' }.isNotEmpty() + } + + /** + * Warning, this method might do a network call and therefore can't be called from + * the UI thread. + */ + @JvmStatic + @Throws(IOException::class) + fun getTrustedNode(ctx: Context): InetAddress? { + var trustedNode: String = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return null + trustedNode = trustedNode.trim { it <= ' ' } + if (trustedNode.isEmpty()) return null + + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$".toRegex())) { + val index = trustedNode.lastIndexOf(':') + trustedNode = trustedNode.substring(0, index) + } + return InetAddress.getByName(trustedNode) + } + + @JvmStatic + fun getTrustedNodePort(ctx: Context): Int { + var trustedNode: String = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return 8444 + trustedNode = trustedNode.trim { it <= ' ' } + + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$".toRegex())) { + val index = trustedNode.lastIndexOf(':') + val portString = trustedNode.substring(index + 1) + try { + return Integer.parseInt(portString) + } catch (e: NumberFormatException) { + ErrorNotification(ctx) + .setError(R.string.error_invalid_sync_port, portString) + .show() + } + } + return 8444 + } + + @JvmStatic + fun getTimeoutInSeconds(ctx: Context): Long { + val preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT) ?: return 120 + return preference.toLong() + } + + private fun getPreference(ctx: Context, name: String): String? { + val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) + + return preferences.getString(name, null) + } + + @JvmStatic + fun isConnectionAllowed(ctx: Context): Boolean { + return !isWifiOnly(ctx) || !WifiReceiver.isConnectedToMeteredNetwork(ctx) + } + + @JvmStatic + fun isWifiOnly(ctx: Context): Boolean { + val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) + return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true) + } + + @JvmStatic + fun setWifiOnly(ctx: Context, status: Boolean) { + val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) + preferences.edit().putBoolean(PREFERENCE_WIFI_ONLY, status).apply() + } + + @JvmStatic + fun isFullNodeActive(ctx: Context): Boolean { + val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) + return preferences.getBoolean(PREFERENCE_FULL_NODE, false) + } + + @JvmStatic + fun setFullNodeActive(ctx: Context, status: Boolean) { + val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) + preferences.edit().putBoolean(PREFERENCE_FULL_NODE, status).apply() + } +} diff --git a/build.gradle b/build.gradle index 1e3da88..fae0801 100644 --- a/build.gradle +++ b/build.gradle @@ -5,11 +5,13 @@ configurations.all { } buildscript { + ext.kotlin_version = '1.1.3-2' repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0' // NOTE: Do not place your application dependencies here; they belong