Merged changes from the afsnit repo.

The project is now built as an Android project (via Eclipse or
ant). Tests have been moved to a separate project so they can exist
outside the Android build process. A basic Android app structure has
been created. A Bluetooth plugin for Android has been added, and the
Bluetooth plugin for J2SE has been modified to use the same techniques.
This commit is contained in:
akwizgran
2012-10-30 22:10:38 +00:00
parent a66da73d37
commit 2f7e2e16cf
132 changed files with 1651 additions and 11702 deletions

2
src/.gitignore vendored
View File

@@ -1 +1 @@
/build
build

View File

@@ -1,3 +1,25 @@
<project name='api' default='depend'>
<import file='../build-common.xml'/>
<project name='prototype' default='compile'>
<fileset id='prototype-jars' dir='../libs'>
<include name='*.jar'/>
</fileset>
<path id='android-jar'>
<pathelement location='../android.jar'/>
</path>
<path id='prototype-classes'>
<pathelement location='../build'/>
</path>
<target name='clean'>
<delete dir='../build'/>
</target>
<target name='compile'>
<mkdir dir='../build'/>
<javac srcdir='net/sf/briar' destdir='../build' source='1.5'
includeantruntime='false' debug='off'>
<classpath>
<fileset refid='prototype-jars'/>
<path refid='android-jar'/>
<path refid='prototype-classes'/>
</classpath>
</javac>
</target>
</project>

View File

@@ -0,0 +1,19 @@
package net.sf.briar;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TextView;
public class HelloWorldActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView text = new TextView(this);
text.setText("Hello world");
setContentView(text);
Intent intent = new Intent("net.sf.briar.AfsnitService");
startService(intent);
}
}

View File

@@ -0,0 +1,59 @@
package net.sf.briar;
import static android.content.Context.MODE_PRIVATE;
import java.io.File;
import net.sf.briar.api.crypto.Password;
import net.sf.briar.api.db.DatabaseConfig;
import net.sf.briar.api.ui.UiCallback;
import android.content.Context;
import com.google.inject.AbstractModule;
public class HelloWorldModule extends AbstractModule {
private final DatabaseConfig config;
private final UiCallback callback;
public HelloWorldModule(final Context appContext) {
final Password password = new Password() {
public char[] getPassword() {
return "foo bar".toCharArray();
}
};
config = new DatabaseConfig() {
public File getDataDirectory() {
return appContext.getDir("db", MODE_PRIVATE);
}
public Password getPassword() {
return password;
}
public long getMaxSize() {
return Long.MAX_VALUE;
}
};
callback = new UiCallback() {
public int showChoice(String[] options, String... message) {
return -1;
}
public boolean showConfirmationMessage(String... message) {
return false;
}
public void showMessage(String... message) {}
};
}
@Override
protected void configure() {
bind(DatabaseConfig.class).toInstance(config);
bind(UiCallback.class).toInstance(callback);
}
}

View File

@@ -0,0 +1,95 @@
package net.sf.briar;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.android.AndroidModule;
import net.sf.briar.api.crypto.KeyManager;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.plugins.PluginManager;
import net.sf.briar.clock.ClockModule;
import net.sf.briar.crypto.CryptoModule;
import net.sf.briar.db.DatabaseModule;
import net.sf.briar.lifecycle.LifecycleModule;
import net.sf.briar.plugins.PluginsModule;
import net.sf.briar.protocol.ProtocolModule;
import net.sf.briar.protocol.duplex.DuplexProtocolModule;
import net.sf.briar.protocol.simplex.SimplexProtocolModule;
import net.sf.briar.serial.SerialModule;
import net.sf.briar.transport.TransportModule;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class HelloWorldService extends Service implements Runnable {
private static final Logger LOG =
Logger.getLogger(HelloWorldService.class.getName());
private DatabaseComponent db = null;
private KeyManager keyManager = null;
private PluginManager pluginManager = null;
@Override
public void onCreate() {
Thread t = new Thread(this);
t.setDaemon(false);
t.start();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return 0;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public void run() {
Injector i = Guice.createInjector(
new HelloWorldModule(getApplicationContext()),
new AndroidModule(), new ClockModule(), new CryptoModule(),
new DatabaseModule(), new LifecycleModule(),
new PluginsModule(), new ProtocolModule(),
new DuplexProtocolModule(), new SimplexProtocolModule(),
new SerialModule(), new TransportModule());
db = i.getInstance(DatabaseComponent.class);
keyManager = i.getInstance(KeyManager.class);
pluginManager = i.getInstance(PluginManager.class);
try {
// Start...
if(LOG.isLoggable(Level.INFO)) LOG.info("Starting");
db.open(false);
if(LOG.isLoggable(Level.INFO)) LOG.info("Database opened");
keyManager.start();
if(LOG.isLoggable(Level.INFO)) LOG.info("Key manager started");
int pluginsStarted = pluginManager.start(this);
if(LOG.isLoggable(Level.INFO))
LOG.info(pluginsStarted + " plugins started");
// ...sleep...
try {
Thread.sleep(1000);
} catch(InterruptedException ignored) {}
// ...and stop
if(LOG.isLoggable(Level.INFO)) LOG.info("Shutting down");
int pluginsStopped = pluginManager.stop();
if(LOG.isLoggable(Level.INFO))
LOG.info(pluginsStopped + " plugins stopped");
keyManager.stop();
if(LOG.isLoggable(Level.INFO)) LOG.info("Key manager stopped");
db.close();
if(LOG.isLoggable(Level.INFO)) LOG.info("Database closed");
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
}

View File

@@ -0,0 +1,89 @@
package net.sf.briar.android;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
import net.sf.briar.api.android.AndroidExecutor;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.google.inject.Inject;
class AndroidExecutorImpl implements AndroidExecutor {
private static final int SHUTDOWN = 0, RUNNABLE = 1, CALLABLE = 2;
private final Runnable loop;
private final AtomicBoolean started = new AtomicBoolean(false);
private final CountDownLatch startLatch = new CountDownLatch(1);
private volatile Handler handler = null;
@Inject
AndroidExecutorImpl() {
loop = new Runnable() {
public void run() {
Looper.prepare();
handler = new FutureTaskHandler();
startLatch.countDown();
Looper.loop();
}
};
}
private void startIfNecessary() {
if(started.getAndSet(true)) return;
new Thread(loop).start();
try {
startLatch.await();
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public Future<Void> submit(Runnable r) {
startIfNecessary();
Future<Void> f = new FutureTask<Void>(r, null);
Message m = Message.obtain(handler, RUNNABLE, f);
handler.sendMessage(m);
return f;
}
public <V> Future<V> submit(Callable<V> c) {
startIfNecessary();
Future<V> f = new FutureTask<V>(c);
Message m = Message.obtain(handler, RUNNABLE, f);
handler.sendMessage(m);
return f;
}
public void shutdown() {
if(handler != null) {
Message m = Message.obtain(handler, SHUTDOWN);
handler.sendMessage(m);
}
}
private static class FutureTaskHandler extends Handler {
@Override
public void handleMessage(Message m) {
switch(m.what) {
case SHUTDOWN:
Looper.myLooper().quit();
break;
case RUNNABLE:
case CALLABLE:
((FutureTask<?>) m.obj).run();
break;
default:
throw new IllegalArgumentException();
}
}
}
}

View File

@@ -0,0 +1,13 @@
package net.sf.briar.android;
import net.sf.briar.api.android.AndroidExecutor;
import com.google.inject.AbstractModule;
public class AndroidModule extends AbstractModule {
@Override
protected void configure() {
bind(AndroidExecutor.class).to(AndroidExecutorImpl.class);
}
}

View File

@@ -0,0 +1,17 @@
package net.sf.briar.api.android;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* Enables background threads to make Android API calls that must be made from
* a thread with a message queue.
*/
public interface AndroidExecutor {
Future<Void> submit(Runnable r);
<V> Future<V> submit(Callable<V> c);
void shutdown();
}

View File

@@ -1,9 +1,9 @@
package net.sf.briar.api.crypto;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.db.ContactTransport;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ConnectionContext;
import net.sf.briar.api.transport.ContactTransport;
public interface KeyManager {

View File

@@ -23,6 +23,8 @@ import net.sf.briar.api.protocol.SubscriptionUpdate;
import net.sf.briar.api.protocol.Transport;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.protocol.TransportUpdate;
import net.sf.briar.api.transport.ContactTransport;
import net.sf.briar.api.transport.TemporarySecret;
/**
* Encapsulates the database implementation and exposes high-level operations
@@ -152,6 +154,19 @@ public interface DatabaseComponent {
long incrementConnectionCounter(ContactId c, TransportId t, long period)
throws DbException;
/**
* Merges the given configuration with existing configuration for the
* given transport.
*/
void mergeConfig(TransportId t, TransportConfig c) throws DbException;
/**
* Merges the given properties with the existing local properties for the
* given transport.
*/
void mergeLocalProperties(TransportId t, TransportProperties p)
throws DbException;
/** Processes an acknowledgement from the given contact. */
void receiveAck(ContactId c, Ack a) throws DbException;
@@ -179,12 +194,6 @@ public interface DatabaseComponent {
/** Removes a contact (and all associated state) from the database. */
void removeContact(ContactId c) throws DbException;
/**
* Sets the configuration for the given transport, replacing any existing
* configuration for that transport.
*/
void setConfig(TransportId t, TransportConfig c) throws DbException;
/**
* Sets the connection reordering window for the given contact transport
* in the given rotation period.
@@ -192,13 +201,6 @@ public interface DatabaseComponent {
void setConnectionWindow(ContactId c, TransportId t, long period,
long centre, byte[] bitmap) throws DbException;
/**
* Sets the local transport properties for the given transport, replacing
* any existing properties for that transport.
*/
void setLocalProperties(TransportId t, TransportProperties p)
throws DbException;
/** Records the user's rating for the given author. */
void setRating(AuthorId a, Rating r) throws DbException;

View File

@@ -0,0 +1,14 @@
package net.sf.briar.api.db;
import java.io.File;
import net.sf.briar.api.crypto.Password;
public interface DatabaseConfig {
File getDataDirectory();
Password getPassword();
long getMaxSize();
}

View File

@@ -1,15 +0,0 @@
package net.sf.briar.api.db;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import com.google.inject.BindingAnnotation;
/** Annotation for injecting the directory where the database is stored. */
@BindingAnnotation
@Target({ PARAMETER })
@Retention(RUNTIME)
public @interface DatabaseDirectory {}

View File

@@ -1,15 +0,0 @@
package net.sf.briar.api.db;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import com.google.inject.BindingAnnotation;
/** Annotation for injecting the maximum size in bytes of the database. */
@BindingAnnotation
@Target({ PARAMETER })
@Retention(RUNTIME)
public @interface DatabaseMaxSize {}

View File

@@ -1,18 +0,0 @@
package net.sf.briar.api.db;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import com.google.inject.BindingAnnotation;
/**
* Annotation for injecting the password from which the database encryption
* key is derived.
*/
@BindingAnnotation
@Target({ PARAMETER })
@Retention(RUNTIME)
public @interface DatabasePassword {}

View File

@@ -0,0 +1,7 @@
package net.sf.briar.api.db;
/** Thrown when a database operation is attempted and the database is closed. */
public class DbClosedException extends DbException {
private static final long serialVersionUID = -3679248177625310653L;
}

View File

@@ -21,11 +21,13 @@ public interface PluginCallback {
/** Returns the plugin's remote transport properties. */
Map<ContactId, TransportProperties> getRemoteProperties();
/** Stores the plugin's configuration. */
void setConfig(TransportConfig c);
/** Merges the given configuration with the plugin's configuration. */
void mergeConfig(TransportConfig c);
/** Stores the plugin's local transport properties. */
void setLocalProperties(TransportProperties p);
/**
* Merges the given properties with the plugin's local transport properties.
*/
void mergeLocalProperties(TransportProperties p);
/**
* Presents the user with a choice among two or more named options and

View File

@@ -3,6 +3,7 @@ package net.sf.briar.api.plugins;
import java.util.Collection;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import android.content.Context;
public interface PluginManager {
@@ -11,7 +12,7 @@ public interface PluginManager {
* started. This method must not be called until the database has been
* opened.
*/
int start();
int start(Context context);
/**
* Stops the plugins and returns the number of plugins successfully stopped.

View File

@@ -2,8 +2,13 @@ package net.sf.briar.api.plugins.duplex;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import android.content.Context;
public interface DuplexPluginFactory {
DuplexPlugin createPlugin(Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
DuplexPluginCallback callback);
}

View File

@@ -2,8 +2,13 @@ package net.sf.briar.api.plugins.simplex;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import android.content.Context;
public interface SimplexPluginFactory {
SimplexPlugin createPlugin(Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
SimplexPluginCallback callback);
}

View File

@@ -2,7 +2,6 @@ package net.sf.briar.api.transport;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.protocol.TransportId;
/**

View File

@@ -1,4 +1,4 @@
package net.sf.briar.api.db;
package net.sf.briar.api.transport;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.protocol.TransportId;

View File

@@ -1,4 +1,4 @@
package net.sf.briar.api.db;
package net.sf.briar.api.transport;
import static net.sf.briar.api.transport.TransportConstants.CONNECTION_WINDOW_SIZE;
import net.sf.briar.api.ContactId;

View File

@@ -8,11 +8,8 @@ import net.sf.briar.api.ContactId;
import net.sf.briar.api.Rating;
import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.db.ContactTransport;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.MessageHeader;
import net.sf.briar.api.db.Status;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.protocol.AuthorId;
import net.sf.briar.api.protocol.BatchId;
import net.sf.briar.api.protocol.Group;
@@ -21,6 +18,8 @@ import net.sf.briar.api.protocol.Message;
import net.sf.briar.api.protocol.MessageId;
import net.sf.briar.api.protocol.Transport;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ContactTransport;
import net.sf.briar.api.transport.TemporarySecret;
/**
* A low-level interface to the database (DatabaseComponent provides a
@@ -475,6 +474,24 @@ interface Database<T> {
long incrementConnectionCounter(T txn, ContactId c, TransportId t,
long period) throws DbException;
/**
* Merges the given configuration with the existing configuration for the
* given transport.
* <p>
* Locking: transport write.
*/
void mergeConfig(T txn, TransportId t, TransportConfig config)
throws DbException;
/**
* Merges the given properties with the existing local properties for the
* given transport.
* <p>
* Locking: transport write.
*/
void mergeLocalProperties(T txn, TransportId t, TransportProperties p)
throws DbException;
/**
* Removes an outstanding batch that has been acknowledged. Any messages in
* the batch that are still considered outstanding (Status.SENT) with
@@ -544,15 +561,6 @@ interface Database<T> {
*/
void removeVisibility(T txn, ContactId c, GroupId g) throws DbException;
/**
* Sets the configuration for the given transport, replacing any existing
* configuration for that transport.
* <p>
* Locking: transport write.
*/
void setConfig(T txn, TransportId t, TransportConfig config)
throws DbException;
/**
* Sets the connection reordering window for the given contact transport in
* the given rotation period.
@@ -569,15 +577,6 @@ interface Database<T> {
*/
void setExpiryTime(T txn, ContactId c, long expiry) throws DbException;
/**
* Sets the local transport properties for the given transport, replacing
* any existing properties for that transport.
* <p>
* Locking: transport write.
*/
void setLocalProperties(T txn, TransportId t, TransportProperties p)
throws DbException;
/**
* Sets the user's rating for the given author.
* <p>

View File

@@ -5,6 +5,7 @@ import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.api.db.DbClosedException;
import net.sf.briar.api.db.DbException;
class DatabaseCleanerImpl extends TimerTask implements DatabaseCleaner {
@@ -32,6 +33,8 @@ class DatabaseCleanerImpl extends TimerTask implements DatabaseCleaner {
if(callback.shouldCheckFreeSpace()) {
callback.checkFreeSpaceAndClean();
}
} catch(DbClosedException e) {
if(LOG.isLoggable(Level.INFO)) LOG.info("Database closed, exiting");
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
throw new Error(e); // Kill the application

View File

@@ -27,14 +27,11 @@ import net.sf.briar.api.Rating;
import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.clock.Clock;
import net.sf.briar.api.db.ContactTransport;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.MessageHeader;
import net.sf.briar.api.db.NoSuchContactException;
import net.sf.briar.api.db.NoSuchContactTransportException;
import net.sf.briar.api.db.Status;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.db.event.BatchReceivedEvent;
import net.sf.briar.api.db.event.ContactAddedEvent;
import net.sf.briar.api.db.event.ContactRemovedEvent;
@@ -62,6 +59,8 @@ import net.sf.briar.api.protocol.SubscriptionUpdate;
import net.sf.briar.api.protocol.Transport;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.protocol.TransportUpdate;
import net.sf.briar.api.transport.ContactTransport;
import net.sf.briar.api.transport.TemporarySecret;
import com.google.inject.Inject;
@@ -1006,6 +1005,47 @@ DatabaseCleaner.Callback {
}
}
public void mergeConfig(TransportId t, TransportConfig c)
throws DbException {
transportLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
db.mergeConfig(txn, t, c);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
transportLock.writeLock().unlock();
}
}
public void mergeLocalProperties(TransportId t, TransportProperties p)
throws DbException {
boolean changed = false;
transportLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if(!p.equals(db.getLocalProperties(txn, t))) {
db.mergeLocalProperties(txn, t, p);
db.setTransportsModified(txn, clock.currentTimeMillis());
changed = true;
}
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
transportLock.writeLock().unlock();
}
// Call the listeners outside the lock
if(changed) callListeners(new LocalTransportsUpdatedEvent());
}
public void receiveAck(ContactId c, Ack a) throws DbException {
contactLock.readLock().lock();
try {
@@ -1263,23 +1303,6 @@ DatabaseCleaner.Callback {
callListeners(new ContactRemovedEvent(c));
}
public void setConfig(TransportId t, TransportConfig c)
throws DbException {
transportLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
db.setConfig(txn, t, c);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
transportLock.writeLock().unlock();
}
}
public void setConnectionWindow(ContactId c, TransportId t, long period,
long centre, byte[] bitmap) throws DbException {
contactLock.readLock().lock();
@@ -1304,30 +1327,6 @@ DatabaseCleaner.Callback {
}
}
public void setLocalProperties(TransportId t, TransportProperties p)
throws DbException {
boolean changed = false;
transportLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if(!p.equals(db.getLocalProperties(txn, t))) {
db.setLocalProperties(txn, t, p);
db.setTransportsModified(txn, clock.currentTimeMillis());
changed = true;
}
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
transportLock.writeLock().unlock();
}
// Call the listeners outside the lock
if(changed) callListeners(new LocalTransportsUpdatedEvent());
}
public void setRating(AuthorId a, Rating r) throws DbException {
boolean changed;
messageLock.writeLock().lock();

View File

@@ -1,16 +1,12 @@
package net.sf.briar.db;
import java.io.File;
import java.sql.Connection;
import java.util.concurrent.Executor;
import net.sf.briar.api.clock.Clock;
import net.sf.briar.api.crypto.Password;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DatabaseDirectory;
import net.sf.briar.api.db.DatabaseConfig;
import net.sf.briar.api.db.DatabaseExecutor;
import net.sf.briar.api.db.DatabaseMaxSize;
import net.sf.briar.api.db.DatabasePassword;
import net.sf.briar.api.lifecycle.ShutdownManager;
import net.sf.briar.api.protocol.GroupFactory;
import net.sf.briar.api.protocol.PacketFactory;
@@ -46,10 +42,9 @@ public class DatabaseModule extends AbstractModule {
}
@Provides
Database<Connection> getDatabase(@DatabaseDirectory File dir,
@DatabasePassword Password password, @DatabaseMaxSize long maxSize,
Database<Connection> getDatabase(DatabaseConfig config,
GroupFactory groupFactory, Clock clock) {
return new H2Database(dir, password, maxSize, groupFactory, clock);
return new H2Database(config, groupFactory, clock);
}
@Provides @Singleton

View File

@@ -10,13 +10,10 @@ import java.util.Properties;
import net.sf.briar.api.clock.Clock;
import net.sf.briar.api.crypto.Password;
import net.sf.briar.api.db.DatabaseDirectory;
import net.sf.briar.api.db.DatabaseMaxSize;
import net.sf.briar.api.db.DatabasePassword;
import net.sf.briar.api.db.DatabaseConfig;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.protocol.GroupFactory;
import org.apache.commons.io.FileSystemUtils;
import net.sf.briar.util.FileUtils;
import com.google.inject.Inject;
@@ -29,22 +26,19 @@ class H2Database extends JdbcDatabase {
private static final String SECRET_TYPE = "BINARY(32)";
private final File home;
private final Password password;
private final String url;
private final Password password;
private final long maxSize;
@Inject
H2Database(@DatabaseDirectory File dir,
@DatabasePassword Password password,
@DatabaseMaxSize long maxSize,
GroupFactory groupFactory, Clock clock) {
H2Database(DatabaseConfig config, GroupFactory groupFactory, Clock clock) {
super(groupFactory, clock, HASH_TYPE, BINARY_TYPE, COUNTER_TYPE,
SECRET_TYPE);
home = new File(dir, "db");
this.password = password;
home = new File(config.getDataDirectory(), "db");
url = "jdbc:h2:split:" + home.getPath()
+ ";CIPHER=AES;DB_CLOSE_ON_EXIT=false";
this.maxSize = maxSize;
+ ";CIPHER=AES;DB_CLOSE_ON_EXIT=false";
password = config.getPassword();
maxSize = config.getMaxSize();
}
public void open(boolean resume) throws DbException, IOException {
@@ -63,8 +57,7 @@ class H2Database extends JdbcDatabase {
public long getFreeSpace() throws DbException {
try {
File dir = home.getParentFile();
String path = dir.getAbsolutePath();
long free = FileSystemUtils.freeSpaceKb(path) * 1024L;
long free = FileUtils.getFreeSpace(dir);
long used = getDiskSpace(dir);
long quota = maxSize - used;
long min = Math.min(free, quota);

View File

@@ -28,11 +28,9 @@ import net.sf.briar.api.Rating;
import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.clock.Clock;
import net.sf.briar.api.db.ContactTransport;
import net.sf.briar.api.db.DbClosedException;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.MessageHeader;
import net.sf.briar.api.db.Status;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.protocol.AuthorId;
import net.sf.briar.api.protocol.BatchId;
import net.sf.briar.api.protocol.Group;
@@ -42,6 +40,8 @@ import net.sf.briar.api.protocol.Message;
import net.sf.briar.api.protocol.MessageId;
import net.sf.briar.api.protocol.Transport;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ContactTransport;
import net.sf.briar.api.transport.TemporarySecret;
import net.sf.briar.util.FileUtils;
/**
@@ -376,7 +376,7 @@ abstract class JdbcDatabase implements Database<Connection> {
public Connection startTransaction() throws DbException {
Connection txn = null;
synchronized(connections) {
if(closed) throw new DbException();
if(closed) throw new DbClosedException();
txn = connections.poll();
}
try {
@@ -1207,6 +1207,7 @@ abstract class JdbcDatabase implements Database<Connection> {
transports.add(t);
}
t.put(rs.getString(2), rs.getString(3));
lastId = id;
}
rs.close();
ps.close();
@@ -2340,28 +2341,52 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
public void setConfig(Connection txn, TransportId t, TransportConfig c)
public void mergeConfig(Connection txn, TransportId t, TransportConfig c)
throws DbException {
mergeStringMap(txn, t, c, "transportConfigs");
}
public void mergeLocalProperties(Connection txn, TransportId t,
TransportProperties p) throws DbException {
mergeStringMap(txn, t, p, "transportProperties");
}
private void mergeStringMap(Connection txn, TransportId t,
Map<String, String> m, String tableName) throws DbException {
PreparedStatement ps = null;
try {
// Delete any existing config for the given transport
String sql = "DELETE FROM transportConfigs WHERE transportId = ?";
// Update any properties that already exist
String sql = "UPDATE " + tableName + " SET value = ?"
+ " WHERE transportId = ? AND key = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, t.getBytes());
ps.executeUpdate();
ps.close();
// Store the new config
sql = "INSERT INTO transportConfigs (transportId, key, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, t.getBytes());
for(Entry<String, String> e : c.entrySet()) {
ps.setString(2, e.getKey());
ps.setString(3, e.getValue());
ps.setBytes(2, t.getBytes());
for(Entry<String, String> e : m.entrySet()) {
ps.setString(1, e.getValue());
ps.setString(3, e.getKey());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if(batchAffected.length != c.size()) throw new DbStateException();
if(batchAffected.length != m.size()) throw new DbStateException();
for(int i = 0; i < batchAffected.length; i++) {
if(batchAffected[i] > 1) throw new DbStateException();
}
// Insert any properties that don't already exist
sql = "INSERT INTO " + tableName + " (transportId, key, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, t.getBytes());
int updateIndex = 0, inserted = 0;
for(Entry<String, String> e : m.entrySet()) {
if(batchAffected[updateIndex] == 0) {
ps.setString(2, e.getKey());
ps.setString(3, e.getValue());
ps.addBatch();
inserted++;
}
updateIndex++;
}
batchAffected = ps.executeBatch();
if(batchAffected.length != inserted) throw new DbStateException();
for(int i = 0; i < batchAffected.length; i++) {
if(batchAffected[i] != 1) throw new DbStateException();
}
@@ -2411,39 +2436,6 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
public void setLocalProperties(Connection txn, TransportId t,
TransportProperties p) throws DbException {
PreparedStatement ps = null;
try {
// Delete any existing properties for the given transport
String sql = "DELETE FROM transportProperties"
+ " WHERE transportId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, t.getBytes());
ps.executeUpdate();
ps.close();
// Store the new properties
sql = "INSERT INTO transportProperties (transportId, key, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, t.getBytes());
for(Entry<String, String> e : p.entrySet()) {
ps.setString(2, e.getKey());
ps.setString(3, e.getValue());
ps.addBatch();
}
int[] batchAffected = ps.executeBatch();
if(batchAffected.length != p.size()) throw new DbStateException();
for(int i = 0; i < batchAffected.length; i++) {
if(batchAffected[i] != 1) throw new DbStateException();
}
ps.close();
} catch(SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
public Rating setRating(Connection txn, AuthorId a, Rating r)
throws DbException {
PreparedStatement ps = null;

View File

@@ -1,7 +1,7 @@
package net.sf.briar.api.db;
package net.sf.briar.db;
/** The status of a message with respect to a particular contact. */
public enum Status {
enum Status {
/** The message has not been sent, received, or acked. */
NEW,
/** The message has been sent, but not received or acked. */

View File

@@ -1,8 +1,5 @@
package net.sf.briar.plugins;
import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT;
import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTY_LENGTH;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -18,6 +15,7 @@ import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.plugins.Plugin;
@@ -36,6 +34,8 @@ import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ConnectionDispatcher;
import net.sf.briar.api.ui.UiCallback;
import net.sf.briar.util.OsUtils;
import android.content.Context;
import com.google.inject.Inject;
@@ -44,17 +44,25 @@ class PluginManagerImpl implements PluginManager {
private static final Logger LOG =
Logger.getLogger(PluginManagerImpl.class.getName());
private static final String[] SIMPLEX_PLUGIN_FACTORIES = new String[] {
private static final String[] ANDROID_SIMPLEX_FACTORIES = new String[0];
private static final String[] ANDROID_DUPLEX_FACTORIES = new String[] {
"net.sf.briar.plugins.droidtooth.DroidtoothPluginFactory",
"net.sf.briar.plugins.socket.SimpleSocketPluginFactory"
};
private static final String[] J2SE_SIMPLEX_FACTORIES = new String[] {
"net.sf.briar.plugins.file.RemovableDrivePluginFactory"
};
private static final String[] DUPLEX_PLUGIN_FACTORIES = new String[] {
private static final String[] J2SE_DUPLEX_FACTORIES = new String[] {
"net.sf.briar.plugins.bluetooth.BluetoothPluginFactory",
"net.sf.briar.plugins.socket.SimpleSocketPluginFactory",
"net.sf.briar.plugins.tor.TorPluginFactory"
};
private final ExecutorService pluginExecutor;
private final AndroidExecutor androidExecutor;
private final DatabaseComponent db;
private final Poller poller;
private final ConnectionDispatcher dispatcher;
@@ -64,9 +72,11 @@ class PluginManagerImpl implements PluginManager {
@Inject
PluginManagerImpl(@PluginExecutor ExecutorService pluginExecutor,
DatabaseComponent db, Poller poller,
ConnectionDispatcher dispatcher, UiCallback uiCallback) {
AndroidExecutor androidExecutor, DatabaseComponent db,
Poller poller, ConnectionDispatcher dispatcher,
UiCallback uiCallback) {
this.pluginExecutor = pluginExecutor;
this.androidExecutor = androidExecutor;
this.db = db;
this.poller = poller;
this.dispatcher = dispatcher;
@@ -75,17 +85,17 @@ class PluginManagerImpl implements PluginManager {
duplexPlugins = new ArrayList<DuplexPlugin>();
}
public synchronized int start() {
public synchronized int start(Context appContext) {
Set<TransportId> ids = new HashSet<TransportId>();
// Instantiate and start the simplex plugins
for(String s : SIMPLEX_PLUGIN_FACTORIES) {
for(String s : getSimplexPluginFactoryNames()) {
try {
Class<?> c = Class.forName(s);
SimplexPluginFactory factory =
(SimplexPluginFactory) c.newInstance();
SimplexCallback callback = new SimplexCallback();
SimplexPlugin plugin = factory.createPlugin(pluginExecutor,
callback);
androidExecutor, appContext, callback);
if(plugin == null) {
if(LOG.isLoggable(Level.INFO)) {
LOG.info(factory.getClass().getSimpleName()
@@ -111,14 +121,14 @@ class PluginManagerImpl implements PluginManager {
}
}
// Instantiate and start the duplex plugins
for(String s : DUPLEX_PLUGIN_FACTORIES) {
for(String s : getDuplexPluginFactoryNames()) {
try {
Class<?> c = Class.forName(s);
DuplexPluginFactory factory =
(DuplexPluginFactory) c.newInstance();
DuplexCallback callback = new DuplexCallback();
DuplexPlugin plugin = factory.createPlugin(pluginExecutor,
callback);
androidExecutor, appContext, callback);
if(plugin == null) {
if(LOG.isLoggable(Level.INFO)) {
LOG.info(factory.getClass().getSimpleName()
@@ -152,8 +162,20 @@ class PluginManagerImpl implements PluginManager {
return simplexPlugins.size() + duplexPlugins.size();
}
private String[] getSimplexPluginFactoryNames() {
if(OsUtils.isAndroid()) return ANDROID_SIMPLEX_FACTORIES;
return J2SE_SIMPLEX_FACTORIES;
}
private String[] getDuplexPluginFactoryNames() {
if(OsUtils.isAndroid()) return ANDROID_DUPLEX_FACTORIES;
return J2SE_DUPLEX_FACTORIES;
}
public synchronized int stop() {
int stopped = 0;
// Stop the poller
poller.stop();
// Stop the simplex plugins
for(SimplexPlugin plugin : simplexPlugins) {
try {
@@ -163,7 +185,6 @@ class PluginManagerImpl implements PluginManager {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
simplexPlugins.clear();
// Stop the duplex plugins
for(DuplexPlugin plugin : duplexPlugins) {
try {
@@ -173,11 +194,9 @@ class PluginManagerImpl implements PluginManager {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
duplexPlugins.clear();
// Stop the poller
poller.stop();
// Shut down the executor service
// Shut down the executors
pluginExecutor.shutdown();
androidExecutor.shutdown();
// Return the number of plugins successfully stopped
return stopped;
}
@@ -232,38 +251,19 @@ class PluginManagerImpl implements PluginManager {
}
}
public void setConfig(TransportConfig c) {
public void mergeConfig(TransportConfig c) {
assert id != null;
try {
db.setConfig(id, c);
db.mergeConfig(id, c);
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
public void setLocalProperties(TransportProperties p) {
public void mergeLocalProperties(TransportProperties p) {
assert id != null;
if(p.size() > MAX_PROPERTIES_PER_TRANSPORT) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + id + " set too many properties");
return;
}
for(String s : p.keySet()) {
if(s.length() > MAX_PROPERTY_LENGTH) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + id + " set long key: " + s);
return;
}
}
for(String s : p.values()) {
if(s.length() > MAX_PROPERTY_LENGTH) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Plugin " + id + " set long value: " + s);
return;
}
}
try {
db.setLocalProperties(id, p);
db.mergeLocalProperties(id, p);
} catch(DbException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}

View File

@@ -1,54 +0,0 @@
package net.sf.briar.plugins.bluetooth;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import javax.bluetooth.DataElement;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.UUID;
abstract class AbstractListener implements DiscoveryListener {
protected final DiscoveryAgent discoveryAgent;
protected final AtomicInteger searches = new AtomicInteger(1);
protected final CountDownLatch finished = new CountDownLatch(1);
protected AbstractListener(DiscoveryAgent discoveryAgent) {
this.discoveryAgent = discoveryAgent;
}
public void inquiryCompleted(int discoveryType) {
if(searches.decrementAndGet() == 0) finished.countDown();
}
public void serviceSearchCompleted(int transaction, int response) {
if(searches.decrementAndGet() == 0) finished.countDown();
}
protected Object getDataElementValue(Object o) {
if(o instanceof DataElement) {
// Bluecove throws an exception if the type is unknown
try {
return ((DataElement) o).getValue();
} catch(ClassCastException e) {
return null;
}
}
return null;
}
protected void findNestedClassIds(Object o, Collection<String> ids) {
o = getDataElementValue(o);
if(o instanceof Enumeration<?>) {
for(Object o1 : Collections.list((Enumeration<?>) o)) {
findNestedClassIds(o1, ids);
}
} else if(o instanceof UUID) {
ids.add(o.toString());
}
}
}

View File

@@ -1,19 +1,17 @@
package net.sf.briar.plugins.bluetooth;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static javax.bluetooth.DiscoveryAgent.GIAC;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -38,28 +36,29 @@ import net.sf.briar.util.StringUtils;
class BluetoothPlugin implements DuplexPlugin {
// Share an ID with the Android Bluetooth plugin
public static final byte[] TRANSPORT_ID =
StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea"
+ "00a539fd260f08a13a0d8a900cde5e49"
+ "1b4df2ffd42e40c408f2db7868f518aa");
StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea"
+ "00a539fd260f08a13a0d8a900cde5e49"
+ "1b4df2ffd42e40c408f2db7868f518aa");
private static final TransportId ID = new TransportId(TRANSPORT_ID);
private static final Logger LOG =
Logger.getLogger(BluetoothPlugin.class.getName());
Logger.getLogger(BluetoothPlugin.class.getName());
private final Executor pluginExecutor;
private final Clock clock;
private final DuplexPluginCallback callback;
private final long pollingInterval;
private final Object discoveryLock = new Object();
private final Object localPropertiesLock = new Object();
private final ScheduledExecutorService scheduler;
private final Collection<StreamConnectionNotifier> sockets; // Locking: this
private boolean running = false; // Locking: this
private LocalDevice localDevice = null; // Locking: this
private StreamConnectionNotifier socket = null; // Locking: this
// Non-null if running has ever been true
private volatile LocalDevice localDevice = null;
BluetoothPlugin(@PluginExecutor Executor pluginExecutor, Clock clock,
DuplexPluginCallback callback, long pollingInterval) {
this.pluginExecutor = pluginExecutor;
@@ -67,7 +66,6 @@ class BluetoothPlugin implements DuplexPlugin {
this.callback = callback;
this.pollingInterval = pollingInterval;
scheduler = Executors.newScheduledThreadPool(0);
sockets = new ArrayList<StreamConnectionNotifier>();
}
public TransportId getId() {
@@ -76,7 +74,6 @@ class BluetoothPlugin implements DuplexPlugin {
public void start() throws IOException {
// Initialise the Bluetooth stack
LocalDevice localDevice;
try {
localDevice = LocalDevice.getLocalDevice();
} catch(UnsatisfiedLinkError e) {
@@ -86,10 +83,9 @@ class BluetoothPlugin implements DuplexPlugin {
throw new IOException(e.toString());
}
if(LOG.isLoggable(Level.INFO))
LOG.info("Address " + localDevice.getBluetoothAddress());
LOG.info("Local address " + localDevice.getBluetoothAddress());
synchronized(this) {
running = true;
this.localDevice = localDevice;
}
pluginExecutor.execute(new Runnable() {
public void run() {
@@ -102,8 +98,11 @@ class BluetoothPlugin implements DuplexPlugin {
synchronized(this) {
if(!running) return;
}
makeDeviceDiscoverable();
String url = "btspp://localhost:" + getUuid() + ";name=RFCOMM";
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put("address", localDevice.getBluetoothAddress());
callback.mergeLocalProperties(p);
String url = makeUrl("localhost", getUuid());
StreamConnectionNotifier scn;
try {
scn = (StreamConnectionNotifier) Connector.open(url);
@@ -121,44 +120,13 @@ class BluetoothPlugin implements DuplexPlugin {
acceptContactConnections(scn);
}
private String getUuid() {
// FIXME: Avoid making alien calls with a lock held
synchronized(localPropertiesLock) {
TransportProperties p = callback.getLocalProperties();
String uuid = p.get("uuid");
if(uuid == null) {
// Generate a (weakly) random UUID and store it
byte[] b = new byte[16];
new Random().nextBytes(b);
uuid = generateUuid(b);
p.put("uuid", uuid);
callback.setLocalProperties(p);
}
return uuid;
}
private String makeUrl(String address, String uuid) {
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
}
private void makeDeviceDiscoverable() {
// Try to make the device discoverable (requires root on Linux)
LocalDevice localDevice;
synchronized(this) {
if(!running) return;
localDevice = this.localDevice;
}
try {
localDevice.setDiscoverable(DiscoveryAgent.GIAC);
} catch(BluetoothStateException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
// Advertise the address to contacts if the device is discoverable
if(localDevice.getDiscoverable() != DiscoveryAgent.NOT_DISCOVERABLE) {
// FIXME: Avoid making alien calls with a lock held
synchronized(localPropertiesLock) {
TransportProperties p = callback.getLocalProperties();
p.put("address", localDevice.getBluetoothAddress());
callback.setLocalProperties(p);
}
}
// FIXME: Get the UUID from the local transport properties
private String getUuid() {
return UUID.nameUUIDFromBytes(new byte[0]).toString();
}
private void tryToClose(StreamConnectionNotifier scn) {
@@ -181,7 +149,7 @@ class BluetoothPlugin implements DuplexPlugin {
return;
}
BluetoothTransportConnection conn =
new BluetoothTransportConnection(s);
new BluetoothTransportConnection(s);
callback.incomingConnectionCreated(conn);
synchronized(this) {
if(!running) return;
@@ -192,9 +160,6 @@ class BluetoothPlugin implements DuplexPlugin {
public void stop() {
synchronized(this) {
running = false;
localDevice = null;
for(StreamConnectionNotifier scn : sockets) tryToClose(scn);
sockets.clear();
if(socket != null) {
tryToClose(socket);
socket = null;
@@ -215,75 +180,31 @@ class BluetoothPlugin implements DuplexPlugin {
synchronized(this) {
if(!running) return;
}
pluginExecutor.execute(new Runnable() {
public void run() {
connectAndCallBack(connected);
}
});
}
private void connectAndCallBack(Collection<ContactId> connected) {
synchronized(this) {
if(!running) return;
}
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
Map<ContactId, String> discovered = discoverContactUrls(remote);
for(Entry<ContactId, String> e : discovered.entrySet()) {
ContactId c = e.getKey();
// Don't create redundant connections
if(connected.contains(c)) continue;
String url = e.getValue();
DuplexTransportConnection d = connect(c, url);
if(d != null) callback.outgoingConnectionCreated(c, d);
}
}
private Map<ContactId, String> discoverContactUrls(
Map<ContactId, TransportProperties> remote) {
LocalDevice localDevice;
synchronized(this) {
if(!running) return Collections.emptyMap();
localDevice = this.localDevice;
}
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
Map<String, ContactId> addresses = new HashMap<String, ContactId>();
Map<ContactId, String> uuids = new HashMap<ContactId, String>();
callback.getRemoteProperties();
for(Entry<ContactId, TransportProperties> e : remote.entrySet()) {
ContactId c = e.getKey();
TransportProperties p = e.getValue();
String address = p.get("address");
String uuid = p.get("uuid");
final ContactId c = e.getKey();
if(connected.contains(c)) continue;
final String address = e.getValue().get("address");
final String uuid = e.getValue().get("uuid");
if(address != null && uuid != null) {
addresses.put(address, c);
uuids.put(c, uuid);
}
}
if(addresses.isEmpty()) return Collections.emptyMap();
ContactListener listener = new ContactListener(discoveryAgent,
Collections.unmodifiableMap(addresses),
Collections.unmodifiableMap(uuids));
// FIXME: Avoid making alien calls with a lock held
synchronized(discoveryLock) {
try {
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener);
return listener.waitForUrls();
} catch(BluetoothStateException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
return Collections.emptyMap();
} catch(InterruptedException e) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Interrupted while waiting for URLs");
Thread.currentThread().interrupt();
return Collections.emptyMap();
pluginExecutor.execute(new Runnable() {
public void run() {
synchronized(BluetoothPlugin.this) {
if(!running) return;
}
String url = makeUrl(address, uuid);
DuplexTransportConnection conn = connect(url);
if(conn != null)
callback.outgoingConnectionCreated(c, conn);
}
});
}
}
}
private DuplexTransportConnection connect(ContactId c, String url) {
synchronized(this) {
if(!running) return null;
}
private DuplexTransportConnection connect(String url) {
try {
StreamConnection s = (StreamConnection) Connector.open(url);
return new BluetoothTransportConnection(s);
@@ -297,12 +218,13 @@ class BluetoothPlugin implements DuplexPlugin {
synchronized(this) {
if(!running) return null;
}
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
if(!remote.containsKey(c)) return null;
remote = Collections.singletonMap(c, remote.get(c));
String url = discoverContactUrls(remote).get(c);
return url == null ? null : connect(c, url);
TransportProperties p = callback.getRemoteProperties().get(c);
if(p == null) return null;
String address = p.get("address");
String uuid = p.get("uuid");
if(address == null || uuid == null) return null;
String url = makeUrl(address, uuid);
return connect(url);
}
public boolean supportsInvitations() {
@@ -311,43 +233,40 @@ class BluetoothPlugin implements DuplexPlugin {
public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) {
return createInvitationConnection(r, timeout);
}
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
return createInvitationConnection(r, timeout);
}
private DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return null;
}
// Use the invitation code to generate the UUID
String uuid = generateUuid(r.nextBytes(16));
// The invitee's device may not be discoverable, so both parties must
// try to initiate connections
final ConnectionCallback c = new ConnectionCallback(uuid, timeout);
pluginExecutor.execute(new Runnable() {
public void run() {
createInvitationConnection(c);
// Discover nearby devices and connect to any with the right UUID
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
long end = clock.currentTimeMillis() + timeout;
String url = null;
while(url == null && clock.currentTimeMillis() < end) {
InvitationListener listener =
new InvitationListener(discoveryAgent, uuid);
// FIXME: Avoid making alien calls with a lock held
synchronized(discoveryLock) {
try {
discoveryAgent.startInquiry(GIAC, listener);
url = listener.waitForUrl();
} catch(BluetoothStateException e) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning(e.toString());
return null;
} catch(InterruptedException e) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Interrupted while waiting for URL");
Thread.currentThread().interrupt();
return null;
}
}
});
pluginExecutor.execute(new Runnable() {
public void run() {
bindInvitationSocket(c);
synchronized(this) {
if(!running) return null;
}
});
try {
StreamConnection s = c.waitForConnection();
return s == null ? null : new BluetoothTransportConnection(s);
} catch(InterruptedException e) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Interrupted while waiting for connection");
Thread.currentThread().interrupt();
return null;
}
if(url == null) return null;
return connect(url);
}
private String generateUuid(byte[] b) {
@@ -355,95 +274,59 @@ class BluetoothPlugin implements DuplexPlugin {
return uuid.toString().replaceAll("-", "");
}
private void createInvitationConnection(ConnectionCallback c) {
LocalDevice localDevice;
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return;
localDevice = this.localDevice;
}
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
// Try to discover the other party until the invitation times out
long end = clock.currentTimeMillis() + c.getTimeout();
String url = null;
while(url == null && clock.currentTimeMillis() < end) {
InvitationListener listener = new InvitationListener(discoveryAgent,
c.getUuid());
// FIXME: Avoid making alien calls with a lock held
synchronized(discoveryLock) {
try {
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener);
url = listener.waitForUrl();
} catch(BluetoothStateException e) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning(e.toString());
return;
} catch(InterruptedException e) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Interrupted while waiting for URL");
Thread.currentThread().interrupt();
return;
}
}
synchronized(this) {
if(!running) return;
}
}
if(url == null) return;
// Try to connect to the other party
try {
StreamConnection s = (StreamConnection) Connector.open(url);
c.addConnection(s);
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
private void bindInvitationSocket(final ConnectionCallback c) {
synchronized(this) {
if(!running) return;
if(!running) return null;
}
// Use the invitation code to generate the UUID
String uuid = generateUuid(r.nextBytes(16));
String url = makeUrl("localhost", uuid);
// Make the device discoverable if possible
makeDeviceDiscoverable();
String url = "btspp://localhost:" + c.getUuid() + ";name=RFCOMM";
// Bind a socket for accepting the invitation connection
final StreamConnectionNotifier scn;
try {
scn = (StreamConnectionNotifier) Connector.open(url);
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
return;
return null;
}
synchronized(this) {
if(!running) {
tryToClose(scn);
return;
return null;
}
sockets.add(scn);
}
// Close the socket when the invitation times out
Runnable close = new Runnable() {
public void run() {
synchronized(this) {
sockets.remove(scn);
}
tryToClose(scn);
}
};
ScheduledFuture<?> future = scheduler.schedule(close,
c.getTimeout(), TimeUnit.MILLISECONDS);
// Try to accept a connection
ScheduledFuture<?> f = scheduler.schedule(close, timeout, MILLISECONDS);
// Try to accept a connection and close the socket
try {
StreamConnection s = scn.acceptAndOpen();
// Close the socket and return the connection
if(future.cancel(false)) {
synchronized(this) {
sockets.remove(scn);
}
tryToClose(scn);
}
c.addConnection(s);
return new BluetoothTransportConnection(s);
} catch(IOException e) {
// This is expected when the socket is closed
if(LOG.isLoggable(Level.INFO)) LOG.info(e.toString());
tryToClose(scn);
return null;
} finally {
if(f.cancel(false)) tryToClose(scn);
}
}
private void makeDeviceDiscoverable() {
// Try to make the device discoverable (requires root on Linux)
synchronized(this) {
if(!running) return;
}
try {
localDevice.setDiscoverable(GIAC);
} catch(BluetoothStateException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
}

View File

@@ -2,17 +2,20 @@ package net.sf.briar.plugins.bluetooth;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.clock.SystemClock;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexPluginFactory;
import android.content.Context;
public class BluetoothPluginFactory implements DuplexPluginFactory {
private static final long POLLING_INTERVAL = 3L * 60L * 1000L; // 3 mins
public DuplexPlugin createPlugin(@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
DuplexPluginCallback callback) {
return new BluetoothPlugin(pluginExecutor, new SystemClock(), callback,
POLLING_INTERVAL);

View File

@@ -1,57 +0,0 @@
package net.sf.briar.plugins.bluetooth;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.microedition.io.StreamConnection;
class ConnectionCallback {
private static final Logger LOG =
Logger.getLogger(ConnectionCallback.class.getName());
private final String uuid;
private final long timeout;
private final long end;
private StreamConnection connection = null; // Locking: this
ConnectionCallback(String uuid, long timeout) {
this.uuid = uuid;
this.timeout = timeout;
end = System.currentTimeMillis() + timeout;
}
String getUuid() {
return uuid;
}
long getTimeout() {
return timeout;
}
synchronized StreamConnection waitForConnection()
throws InterruptedException {
long now = System.currentTimeMillis();
while(connection == null && now < end) {
wait(end - now);
now = System.currentTimeMillis();
}
return connection;
}
synchronized void addConnection(StreamConnection s) {
if(connection == null) {
connection = s;
notifyAll();
} else {
// Redundant connection
try {
s.close();
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
}
}

View File

@@ -1,83 +0,0 @@
package net.sf.briar.plugins.bluetooth;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import net.sf.briar.api.ContactId;
class ContactListener extends AbstractListener {
private static final Logger LOG =
Logger.getLogger(ContactListener.class.getName());
private final Map<String, ContactId> addresses;
private final Map<ContactId, String> uuids;
private final Map<ContactId, String> urls;
ContactListener(DiscoveryAgent discoveryAgent,
Map<String, ContactId> addresses, Map<ContactId, String> uuids) {
super(discoveryAgent);
this.addresses = addresses;
this.uuids = uuids;
urls = Collections.synchronizedMap(new HashMap<ContactId, String>());
}
Map<ContactId, String> waitForUrls() throws InterruptedException {
finished.await();
return urls;
}
public void deviceDiscovered(RemoteDevice device, DeviceClass deviceClass) {
// Do we recognise the address?
ContactId contactId = addresses.get(device.getBluetoothAddress());
if(contactId == null) return;
// Do we have a UUID for this contact?
String uuid = uuids.get(contactId);
if(uuid == null) return;
UUID[] uuids = new UUID[] { new UUID(uuid, false) };
// Try to discover the services associated with the UUID
try {
discoveryAgent.searchServices(null, uuids, device, this);
} catch(BluetoothStateException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
searches.incrementAndGet();
}
public void servicesDiscovered(int transaction, ServiceRecord[] services) {
for(ServiceRecord record : services) {
// Do we recognise the address?
RemoteDevice device = record.getHostDevice();
ContactId c = addresses.get(device.getBluetoothAddress());
if(c == null) continue;
// Do we have a UUID for this contact?
String uuid = uuids.get(c);
if(uuid == null) return;
// Does this service have a URL?
String serviceUrl = record.getConnectionURL(
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
if(serviceUrl == null) continue;
// Does this service have the UUID we're looking for?
Collection<String> uuids = new TreeSet<String>();
findNestedClassIds(record.getAttributeValue(0x1), uuids);
for(String u : uuids) {
if(uuid.equalsIgnoreCase(u.toString())) {
// The UUID matches - store the URL
urls.put(c, serviceUrl);
}
}
}
}
}

View File

@@ -1,28 +1,37 @@
package net.sf.briar.plugins.bluetooth;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DataElement;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
class InvitationListener extends AbstractListener {
class InvitationListener implements DiscoveryListener {
private static final Logger LOG =
Logger.getLogger(InvitationListener.class.getName());
Logger.getLogger(InvitationListener.class.getName());
private final AtomicInteger searches = new AtomicInteger(1);
private final CountDownLatch finished = new CountDownLatch(1);
private final DiscoveryAgent discoveryAgent;
private final String uuid;
private volatile String url = null;
InvitationListener(DiscoveryAgent discoveryAgent, String uuid) {
super(discoveryAgent);
this.discoveryAgent = discoveryAgent;
this.uuid = uuid;
}
@@ -61,4 +70,35 @@ class InvitationListener extends AbstractListener {
}
}
}
public void inquiryCompleted(int discoveryType) {
if(searches.decrementAndGet() == 0) finished.countDown();
}
public void serviceSearchCompleted(int transaction, int response) {
if(searches.decrementAndGet() == 0) finished.countDown();
}
// UUIDs are sometimes buried in nested data elements
private void findNestedClassIds(Object o, Collection<String> ids) {
o = getDataElementValue(o);
if(o instanceof Enumeration<?>) {
for(Object o1 : Collections.list((Enumeration<?>) o))
findNestedClassIds(o1, ids);
} else if(o instanceof UUID) {
ids.add(o.toString());
}
}
private Object getDataElementValue(Object o) {
if(o instanceof DataElement) {
// Bluecove throws an exception if the type is unknown
try {
return ((DataElement) o).getValue();
} catch(ClassCastException e) {
return null;
}
}
return null;
}
}

View File

@@ -0,0 +1,431 @@
package net.sf.briar.plugins.droidtooth;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.util.StringUtils;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
class DroidtoothPlugin implements DuplexPlugin {
// Share an ID with the J2SE Bluetooth plugin
public static final byte[] TRANSPORT_ID =
StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea"
+ "00a539fd260f08a13a0d8a900cde5e49"
+ "1b4df2ffd42e40c408f2db7868f518aa");
private static final TransportId ID = new TransportId(TRANSPORT_ID);
private static final Logger LOG =
Logger.getLogger(DroidtoothPlugin.class.getName());
private static final String FOUND = "android.bluetooth.device.action.FOUND";
private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private final Executor pluginExecutor;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final DuplexPluginCallback callback;
private final long pollingInterval;
private boolean running = false; // Locking: this
private BluetoothServerSocket socket = null; // Locking: this
// Non-null if running has ever been true
private volatile BluetoothAdapter adapter = null;
DroidtoothPlugin(@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
DuplexPluginCallback callback, long pollingInterval) {
this.pluginExecutor = pluginExecutor;
this.androidExecutor = androidExecutor;
this.appContext = appContext;
this.callback = callback;
this.pollingInterval = pollingInterval;
}
public TransportId getId() {
return ID;
}
public void start() throws IOException {
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
// with a message queue, so submit it to the AndroidExecutor
Callable<BluetoothAdapter> c = new Callable<BluetoothAdapter>() {
public BluetoothAdapter call() throws Exception {
return BluetoothAdapter.getDefaultAdapter();
}
};
Future<BluetoothAdapter> f = androidExecutor.submit(c);
try {
adapter = f.get();
} catch(InterruptedException e) {
throw new IOException(e.toString());
} catch(ExecutionException e) {
throw new IOException(e.toString());
}
if(adapter == null) throw new IOException(); // Bluetooth not supported
synchronized(this) {
running = true;
}
pluginExecutor.execute(new Runnable() {
public void run() {
bind();
}
});
}
private void bind() {
synchronized(this) {
if(!running) return;
}
if(!enableBluetooth()) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Could not enable Bluetooth");
return;
}
makeDeviceDiscoverable();
if(LOG.isLoggable(Level.INFO))
LOG.info("Local address " + adapter.getAddress());
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put("address", adapter.getAddress());
callback.mergeLocalProperties(p);
// Bind a server socket to accept connections from contacts
BluetoothServerSocket ss;
try {
ss = InsecureBluetooth.listen(adapter, "RFCOMM", getUuid(), false);
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
return;
}
synchronized(this) {
if(!running) {
tryToClose(ss);
return;
}
socket = ss;
}
acceptContactConnections(ss);
}
private boolean enableBluetooth() {
synchronized(this) {
if(!running) return false;
}
if(adapter.isEnabled()) return true;
// Try to enable the adapter and wait for the result
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
BluetoothStateReceiver receiver = new BluetoothStateReceiver();
appContext.registerReceiver(receiver, filter);
try {
if(!adapter.enable()) return false;
return receiver.waitForStateChange();
} catch(InterruptedException e) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Interrupted while enabling Bluetooth");
Thread.currentThread().interrupt();
return false;
}
}
private void makeDeviceDiscoverable() {
synchronized(this) {
if(!running) return;
}
if(adapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) return;
// Indefinite discoverability can only be set on API Level 8 or higher
if(Build.VERSION.SDK_INT < 8) return;
Intent intent = new Intent(ACTION_REQUEST_DISCOVERABLE);
intent.putExtra(EXTRA_DISCOVERABLE_DURATION, 0);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
appContext.startActivity(intent);
}
// FIXME: Get the UUID from the local transport properties
private UUID getUuid() {
return UUID.nameUUIDFromBytes(new byte[0]);
}
private void tryToClose(BluetoothServerSocket ss) {
try {
ss.close();
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
}
}
private void acceptContactConnections(BluetoothServerSocket ss) {
while(true) {
BluetoothSocket s;
try {
s = ss.accept();
} catch(IOException e) {
// This is expected when the socket is closed
if(LOG.isLoggable(Level.INFO)) LOG.info(e.toString());
tryToClose(ss);
return;
}
DroidtoothTransportConnection conn =
new DroidtoothTransportConnection(s);
callback.incomingConnectionCreated(conn);
synchronized(this) {
if(!running) return;
}
}
}
public void stop() throws IOException {
synchronized(this) {
running = false;
if(socket != null) {
tryToClose(socket);
socket = null;
}
}
}
public boolean shouldPoll() {
return true;
}
public long getPollingInterval() {
return pollingInterval;
}
public void poll(Collection<ContactId> connected) {
synchronized(this) {
if(!running) return;
}
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for(Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if(connected.contains(c)) continue;
final String address = e.getValue().get("address");
final String uuid = e.getValue().get("uuid");
if(address != null && uuid != null) {
pluginExecutor.execute(new Runnable() {
public void run() {
synchronized(DroidtoothPlugin.this) {
if(!running) return;
}
DuplexTransportConnection conn = connect(address, uuid);
if(conn != null)
callback.outgoingConnectionCreated(c, conn);
}
});
}
}
}
private DuplexTransportConnection connect(String address, String uuid) {
// Validate the address
if(!BluetoothAdapter.checkBluetoothAddress(address)) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Invalid address " + address);
return null;
}
BluetoothDevice d = adapter.getRemoteDevice(address);
// Validate the UUID
UUID u;
try {
u = UUID.fromString(uuid);
} catch(IllegalArgumentException e) {
if(LOG.isLoggable(Level.WARNING))
LOG.warning("Invalid UUID " + uuid);
return null;
}
// Try to connect
try {
BluetoothSocket s = InsecureBluetooth.createSocket(d, u, false);
return new DroidtoothTransportConnection(s);
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
return null;
}
}
public DuplexTransportConnection createConnection(ContactId c) {
synchronized(this) {
if(!running) return null;
}
TransportProperties p = callback.getRemoteProperties().get(c);
if(p == null) return null;
String address = p.get("address");
String uuid = p.get("uuid");
if(address == null || uuid == null) return null;
return connect(address, uuid);
}
public boolean supportsInvitations() {
return true;
}
public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return null;
}
// Use the same pseudo-random UUID as the contact
String uuid = UUID.nameUUIDFromBytes(r.nextBytes(16)).toString();
// Register to receive Bluetooth discovery intents
IntentFilter filter = new IntentFilter();
filter.addAction(FOUND);
filter.addAction(DISCOVERY_FINISHED);
// Discover nearby devices and connect to any with the right UUID
DiscoveryReceiver receiver = new DiscoveryReceiver(uuid);
appContext.registerReceiver(receiver, filter);
adapter.startDiscovery();
try {
return receiver.waitForConnection(timeout);
} catch(InterruptedException e) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Interrupted while sending invitation");
Thread.currentThread().interrupt();
return null;
}
}
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return null;
}
// Use the same pseudo-random UUID as the contact
UUID uuid = UUID.nameUUIDFromBytes(r.nextBytes(16));
// Bind a new server socket to accept the invitation connection
final BluetoothServerSocket ss;
try {
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid, false);
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
return null;
}
// Return the first connection received by the socket, if any
try {
return new DroidtoothTransportConnection(ss.accept((int) timeout));
} catch(SocketTimeoutException e) {
if(LOG.isLoggable(Level.INFO)) LOG.info("Invitation timed out");
return null;
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
return null;
} finally {
tryToClose(ss);
}
}
private static class BluetoothStateReceiver extends BroadcastReceiver {
private final CountDownLatch finished = new CountDownLatch(1);
private volatile boolean enabled = false;
@Override
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if(state == STATE_ON) {
enabled = true;
finish(ctx);
} else if(state == STATE_OFF) {
finish(ctx);
}
}
private void finish(Context ctx) {
ctx.getApplicationContext().unregisterReceiver(this);
finished.countDown();
}
boolean waitForStateChange() throws InterruptedException {
finished.await();
return enabled;
}
}
private class DiscoveryReceiver extends BroadcastReceiver {
private final CountDownLatch finished = new CountDownLatch(1);
private final String uuid;
private volatile DuplexTransportConnection connection = null;
private DiscoveryReceiver(String uuid) {
this.uuid = uuid;
}
@Override
public void onReceive(final Context ctx, Intent intent) {
String action = intent.getAction();
if(action.equals(DISCOVERY_FINISHED)) {
finish(ctx);
} else if(action.equals(FOUND)) {
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
final String address = d.getAddress();
pluginExecutor.execute(new Runnable() {
public void run() {
synchronized(DroidtoothPlugin.this) {
if(!running) return;
}
DuplexTransportConnection conn = connect(address, uuid);
if(conn != null) {
connection = conn;
finish(ctx);
}
}
});
}
}
private void finish(Context ctx) {
ctx.getApplicationContext().unregisterReceiver(this);
finished.countDown();
}
private DuplexTransportConnection waitForConnection(long timeout)
throws InterruptedException {
finished.await(timeout, MILLISECONDS);
return connection;
}
}
}

View File

@@ -0,0 +1,22 @@
package net.sf.briar.plugins.droidtooth;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexPluginFactory;
import android.content.Context;
public class DroidtoothPluginFactory implements DuplexPluginFactory {
private static final long POLLING_INTERVAL = 3L * 60L * 1000L; // 3 mins
public DuplexPlugin createPlugin(@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
DuplexPluginCallback callback) {
return new DroidtoothPlugin(pluginExecutor, androidExecutor, appContext,
callback, POLLING_INTERVAL);
}
}

View File

@@ -0,0 +1,34 @@
package net.sf.briar.plugins.droidtooth;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
import android.bluetooth.BluetoothSocket;
class DroidtoothTransportConnection implements DuplexTransportConnection {
private final BluetoothSocket socket;
DroidtoothTransportConnection(BluetoothSocket socket) {
this.socket = socket;
}
public InputStream getInputStream() throws IOException {
return socket.getInputStream();
}
public OutputStream getOutputStream() throws IOException {
return socket.getOutputStream();
}
public boolean shouldFlush() {
return true;
}
public void dispose(boolean exception, boolean recognised)
throws IOException {
socket.close();
}
}

View File

@@ -0,0 +1,207 @@
package net.sf.briar.plugins.droidtooth;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelUuid;
// Based on http://stanford.edu/~tpurtell/InsecureBluetooth.java by T.J. Purtell
class InsecureBluetooth {
static BluetoothServerSocket listen(BluetoothAdapter adapter, String name,
UUID uuid, boolean encrypt) throws IOException {
try {
String className = BluetoothAdapter.class.getName()
+ ".RfcommChannelPicker";
Class<?> channelPickerClass = null;
Class<?>[] children = BluetoothAdapter.class.getDeclaredClasses();
for(Class<?> c : children) {
if(c.getCanonicalName().equals(className)) {
channelPickerClass = c;
break;
}
}
if(channelPickerClass == null)
throw new IOException("Can't find channel picker class");
Constructor<?> constructor =
channelPickerClass.getDeclaredConstructor(UUID.class);
if(constructor == null)
throw new IOException("Can't find channel picker constructor");
Object channelPicker = constructor.newInstance(uuid);
Method nextChannel = channelPickerClass.getDeclaredMethod(
"nextChannel", new Class[0]);
nextChannel.setAccessible(true);
BluetoothServerSocket socket = null;
int channel;
while(true) {
channel = (Integer) nextChannel.invoke(channelPicker,
new Object[0]);
if(channel == -1)
throw new IOException("No available channels");
try {
socket = listen(channel, encrypt);
break;
} catch(InUseException e) {
continue;
}
}
Field f = adapter.getClass().getDeclaredField("mService");
f.setAccessible(true);
Object mService = f.get(adapter);
Method addRfcommServiceRecord =
mService.getClass().getDeclaredMethod(
"addRfcommServiceRecord", String.class,
ParcelUuid.class, int.class, IBinder.class);
addRfcommServiceRecord.setAccessible(true);
int handle = (Integer) addRfcommServiceRecord.invoke(mService, name,
new ParcelUuid(uuid), channel, new Binder());
if(handle == -1) {
try {
socket.close();
} catch(IOException ignored) {}
throw new IOException("Can't register SDP record for " + name);
}
Field f1 = adapter.getClass().getDeclaredField("mHandler");
f1.setAccessible(true);
Object mHandler = f1.get(adapter);
Method setCloseHandler = socket.getClass().getDeclaredMethod(
"setCloseHandler", Handler.class, int.class);
setCloseHandler.setAccessible(true);
setCloseHandler.invoke(socket, mHandler, handle);
return socket;
} catch(NoSuchMethodException e) {
throw new IOException(e.toString());
} catch(NoSuchFieldException e) {
throw new IOException(e.toString());
} catch(IllegalAccessException e) {
throw new IOException(e.toString());
} catch(InstantiationException e) {
throw new IOException(e.toString());
} catch(InvocationTargetException e) {
if(e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
} else {
throw new IOException(e.toString());
}
}
}
private static BluetoothServerSocket listen(int port, boolean encrypt,
boolean reuse) throws IOException, InUseException {
BluetoothServerSocket socket = null;
try {
Constructor<BluetoothServerSocket> constructor =
BluetoothServerSocket.class.getDeclaredConstructor(
int.class, boolean.class, boolean.class, int.class);
if(constructor == null)
throw new IOException("Can't find server socket constructor");
constructor.setAccessible(true);
Field f = BluetoothSocket.class.getDeclaredField("TYPE_RFCOMM");
f.setAccessible(true);
int rfcommType = (Integer) f.get(null);
Field f1 = BluetoothSocket.class.getDeclaredField("EADDRINUSE");
f1.setAccessible(true);
int eAddrInUse = (Integer) f1.get(null);
socket = constructor.newInstance(rfcommType, false, encrypt, port);
Field f2 = socket.getClass().getDeclaredField("mSocket");
f2.setAccessible(true);
Object mSocket = f2.get(socket);
Method bindListen = mSocket.getClass().getDeclaredMethod(
"bindListen", new Class[0]);
bindListen.setAccessible(true);
Object result = bindListen.invoke(mSocket, new Object[0]);
int errno = (Integer) result;
if(reuse && errno == eAddrInUse) {
throw new InUseException();
} else if(errno != 0) {
try {
socket.close();
} catch(IOException ignored) {}
Method throwErrnoNative = mSocket.getClass().getMethod(
"throwErrnoNative", int.class);
throwErrnoNative.invoke(mSocket, errno);
}
return socket;
} catch(NoSuchMethodException e) {
throw new IOException(e.toString());
} catch(NoSuchFieldException e) {
throw new IOException(e.toString());
} catch(IllegalAccessException e) {
throw new IOException(e.toString());
} catch(InstantiationException e) {
throw new IOException(e.toString());
} catch(InvocationTargetException e) {
if(e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
} else {
throw new IOException(e.toString());
}
}
}
static BluetoothServerSocket listen(int port, boolean encrypt)
throws IOException {
return listen(port, encrypt, false);
}
private static BluetoothSocket createSocket(BluetoothDevice device,
int port, UUID uuid, boolean encrypt) throws IOException {
try {
BluetoothSocket socket = null;
Constructor<BluetoothSocket> constructor =
BluetoothSocket.class.getDeclaredConstructor(int.class,
int.class, boolean.class, boolean.class,
BluetoothDevice.class, int.class, ParcelUuid.class);
if(constructor == null)
throw new IOException("Can't find socket constructor");
constructor.setAccessible(true);
Field f = BluetoothSocket.class.getDeclaredField("TYPE_RFCOMM");
f.setAccessible(true);
int typeRfcomm = (Integer) f.get(null);
socket = constructor.newInstance(typeRfcomm, -1, false, true,
device, port, uuid != null ? new ParcelUuid(uuid) : null);
return socket;
} catch(NoSuchMethodException e) {
throw new IOException(e.toString());
} catch(NoSuchFieldException e) {
throw new IOException(e.toString());
} catch(IllegalAccessException e) {
throw new IOException(e.toString());
} catch(InstantiationException e) {
throw new IOException(e.toString());
} catch(InvocationTargetException e) {
if(e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
} else {
throw new IOException(e.toString());
}
}
}
static BluetoothSocket createSocket(BluetoothDevice device, UUID uuid,
boolean encrypt) throws IOException {
return createSocket(device, -1, uuid, encrypt);
}
static BluetoothSocket createSocket(BluetoothDevice device, int port,
boolean encrypt) throws IOException {
return createSocket(device, port, null, encrypt);
}
private static class InUseException extends RuntimeException {
private static final long serialVersionUID = -5983642322821496023L;
}
}

View File

@@ -2,13 +2,16 @@ package net.sf.briar.plugins.email;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.plugins.simplex.SimplexPlugin;
import net.sf.briar.api.plugins.simplex.SimplexPluginCallback;
import net.sf.briar.api.plugins.simplex.SimplexPluginFactory;
import android.content.Context;
public class GmailPluginFactory implements SimplexPluginFactory {
public SimplexPlugin createPlugin(Executor pluginExecutor,
AndroidExecutor androidExecutor, Context context,
SimplexPluginCallback callback) {
return new GmailPlugin(pluginExecutor, callback);
}

View File

@@ -2,17 +2,20 @@ package net.sf.briar.plugins.file;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.simplex.SimplexPlugin;
import net.sf.briar.api.plugins.simplex.SimplexPluginCallback;
import net.sf.briar.api.plugins.simplex.SimplexPluginFactory;
import net.sf.briar.util.OsUtils;
import android.content.Context;
public class RemovableDrivePluginFactory implements SimplexPluginFactory {
private static final long POLLING_INTERVAL = 10L * 1000L; // 10 seconds
public SimplexPlugin createPlugin(@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
SimplexPluginCallback callback) {
RemovableDriveFinder finder;
RemovableDriveMonitor monitor;

View File

@@ -128,12 +128,12 @@ class SimpleSocketPlugin extends SocketPlugin {
throw new IllegalArgumentException();
InetSocketAddress i = (InetSocketAddress) s;
InetAddress addr = i.getAddress();
TransportProperties p = callback.getLocalProperties();
TransportProperties p = new TransportProperties();
if(addr.isLinkLocalAddress() || addr.isSiteLocalAddress())
p.put("internal", addr.getHostAddress());
else p.put("external", addr.getHostAddress());
p.put("port", String.valueOf(i.getPort()));
callback.setLocalProperties(p);
callback.mergeLocalProperties(p);
}
public boolean supportsInvitations() {

View File

@@ -2,16 +2,19 @@ package net.sf.briar.plugins.socket;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexPluginFactory;
import android.content.Context;
public class SimpleSocketPluginFactory implements DuplexPluginFactory {
private static final long POLLING_INTERVAL = 5L * 60L * 1000L; // 5 mins
public DuplexPlugin createPlugin(@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
DuplexPluginCallback callback) {
return new SimpleSocketPlugin(pluginExecutor, callback,
POLLING_INTERVAL);

View File

@@ -95,9 +95,9 @@ class TorPlugin implements DuplexPlugin {
if(c.containsKey("noHiddenService")) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Not creating hidden service");
TransportProperties p = callback.getLocalProperties();
p.remove("onion");
callback.setLocalProperties(p);
TransportProperties p = new TransportProperties();
p.put("onion", null);
callback.mergeLocalProperties(p);
return;
}
// Retrieve the hidden service address, or create one if necessary
@@ -107,7 +107,7 @@ class TorPlugin implements DuplexPlugin {
if(privateKey == null) {
if(LOG.isLoggable(Level.INFO))
LOG.info("Creating hidden service address");
addr = createHiddenServiceAddress(util, c);
addr = createHiddenServiceAddress(util);
} else {
if(LOG.isLoggable(Level.INFO))
LOG.info("Parsing hidden service address");
@@ -116,7 +116,7 @@ class TorPlugin implements DuplexPlugin {
privateKey, "", false);
} catch(IOException e) {
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
addr = createHiddenServiceAddress(util, c);
addr = createHiddenServiceAddress(util);
}
}
TorHiddenServicePortPrivateNetAddress addrPort =
@@ -141,18 +141,19 @@ class TorPlugin implements DuplexPlugin {
if(LOG.isLoggable(Level.INFO)) LOG.info("Listening on " + onion);
TransportProperties p = callback.getLocalProperties();
p.put("onion", onion);
callback.setLocalProperties(p);
callback.mergeLocalProperties(p);
acceptContactConnections(ss);
}
private TorHiddenServicePrivateNetAddress createHiddenServiceAddress(
TorNetLayerUtil util, TransportConfig c) {
TorNetLayerUtil util) {
TorHiddenServicePrivateNetAddress addr =
util.createNewTorHiddenServicePrivateNetAddress();
RSAKeyPair keyPair = addr.getKeyPair();
String privateKey = Encryption.getPEMStringFromRSAKeyPair(keyPair);
TransportConfig c = new TransportConfig();
c.put("privateKey", privateKey);
callback.setConfig(c);
callback.mergeConfig(c);
return addr;
}

View File

@@ -2,16 +2,19 @@ package net.sf.briar.plugins.tor;
import java.util.concurrent.Executor;
import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexPluginFactory;
import android.content.Context;
public class TorPluginFactory implements DuplexPluginFactory {
private static final long POLLING_INTERVAL = 15L * 60L * 1000L; // 15 mins
public DuplexPlugin createPlugin(@PluginExecutor Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext,
DuplexPluginCallback callback) {
return new TorPlugin(pluginExecutor, callback, POLLING_INTERVAL);
}

View File

@@ -7,10 +7,10 @@ import net.sf.briar.api.ContactId;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ConnectionContext;
import net.sf.briar.api.transport.ConnectionRecogniser;
import net.sf.briar.api.transport.TemporarySecret;
import com.google.inject.Inject;

View File

@@ -14,16 +14,16 @@ import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.KeyManager;
import net.sf.briar.api.db.ContactTransport;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.db.event.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseEvent;
import net.sf.briar.api.db.event.DatabaseListener;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ConnectionContext;
import net.sf.briar.api.transport.ConnectionRecogniser;
import net.sf.briar.api.transport.ContactTransport;
import net.sf.briar.api.transport.TemporarySecret;
import net.sf.briar.util.ByteUtils;
import com.google.inject.Inject;

View File

@@ -15,9 +15,9 @@ import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.ErasableKey;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.TemporarySecret;
import net.sf.briar.api.protocol.TransportId;
import net.sf.briar.api.transport.ConnectionContext;
import net.sf.briar.api.transport.TemporarySecret;
import net.sf.briar.util.ByteUtils;
/** A connection recogniser for a specific transport. */

View File

@@ -1,12 +1,13 @@
package net.sf.briar.util;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -26,8 +27,8 @@ public class BoundedExecutor implements Executor {
public BoundedExecutor(int maxQueued, int minThreads, int maxThreads) {
semaphore = new Semaphore(maxQueued + maxThreads);
queue = new LinkedBlockingQueue<Runnable>();
executor = new ThreadPoolExecutor(minThreads, maxThreads, 60,
TimeUnit.SECONDS, queue);
executor = new ThreadPoolExecutor(minThreads, maxThreads, 60, SECONDS,
queue);
}
public void execute(final Runnable r) {

View File

@@ -7,6 +7,12 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.CodeSource;
import org.apache.commons.io.FileSystemUtils;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.StatFs;
public class FileUtils {
/**
@@ -62,7 +68,7 @@ public class FileUtils {
* callback is not null it's called once for each file created.
*/
public static void copyRecursively(File src, File dest, Callback callback)
throws IOException {
throws IOException {
assert dest.exists();
assert dest.isDirectory();
dest = new File(dest, src.getName());
@@ -82,6 +88,20 @@ public class FileUtils {
f.delete();
}
@SuppressLint("NewApi")
public static long getFreeSpace(File f) throws IOException {
if(OsUtils.isAndroid()) {
if(Build.VERSION.SDK_INT >= 9) {
return f.getUsableSpace();
} else {
StatFs s = new StatFs(f.getAbsolutePath());
return (long) s.getAvailableBlocks() * s.getBlockSize();
}
} else {
return FileSystemUtils.freeSpaceKb(f.getAbsolutePath()) * 1024L;
}
}
public interface Callback {
void processingFile(File f);

View File

@@ -4,13 +4,14 @@ public class OsUtils {
private static final String os = System.getProperty("os.name");
private static final String version = System.getProperty("os.version");
private static final String vendor = System.getProperty("java.vendor");
public static boolean isWindows() {
return os.indexOf("Windows") != -1;
return os != null && os.indexOf("Windows") != -1;
}
public static boolean isMac() {
return os.indexOf("Mac OS") != -1;
return os != null && os.indexOf("Mac OS") != -1;
}
public static boolean isMacLeopardOrNewer() {
@@ -27,6 +28,10 @@ public class OsUtils {
}
public static boolean isLinux() {
return os.indexOf("Linux") != -1;
return os != null && os.indexOf("Linux") != -1 && !isAndroid();
}
public static boolean isAndroid() {
return vendor != null && vendor.indexOf("Android") != -1;
}
}