Bluetooth-only invitations: simpler and more reliable.

Of course, not all devices support Bluetooth...
This commit is contained in:
akwizgran
2014-02-10 14:00:34 +00:00
parent c6ac826acd
commit 044c10e89f
20 changed files with 80 additions and 747 deletions

View File

@@ -11,7 +11,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application

View File

@@ -28,22 +28,13 @@
<string name="format_last_connected">Last connected &lt;br /&gt; %1$s</string>
<string name="add_contact_title">Add a Contact</string>
<string name="your_nickname">Your nickname: </string>
<string name="wifi_not_available">Wi-Fi is NOT AVAILABLE</string>
<string name="wifi_disabled">Wi-Fi is OFF</string>
<string name="wifi_disconnected">Wi-Fi is DISCONNECTED</string>
<string name="format_wifi_connected">Wi-Fi is connected to %1$s</string>
<string name="bluetooth_not_available">Bluetooth is NOT AVAILABLE</string>
<string name="bluetooth_disabled">Bluetooth is OFF</string>
<string name="bluetooth_not_discoverable">Bluetooth is NOT DISCOVERABLE</string>
<string name="bluetooth_discoverable">Bluetooth is discoverable</string>
<string name="fact_to_face">For security reasons you must be face-to-face with someone to add them as a contact</string>
<string name="face_to_face">For security reasons you must be face-to-face with the person you want to add as a contact.\n\nThis will prevent anyone from impersonating you or reading your messages in future.</string>
<string name="continue_button">Continue</string>
<string name="your_invitation_code">Your invitation code is</string>
<string name="enter_invitation_code">Please enter your contact\'s invitation code:</string>
<string name="format_connecting_wifi">Searching via %1$s\u2026</string>
<string name="connecting_bluetooth">Searching via Bluetooth\u2026</string>
<string name="searching_format">Searching for %1$s\u2026</string>
<string name="connection_failed">Connection failed</string>
<string name="check_same_network">Please check that you are both using the same network</string>
<string name="could_not_find_contact">Briar could not find your contact nearby</string>
<string name="try_again_button">Try again</string>
<string name="connected_to_contact">Connected to contact</string>
<string name="calculating_confirmation_code">Calculating confirmation code\u2026</string>

View File

@@ -1,10 +1,5 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.net.wifi.WifiManager.NETWORK_STATE_CHANGED_ACTION;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@@ -33,13 +28,7 @@ import org.briarproject.api.invitation.InvitationTask;
import org.briarproject.api.invitation.InvitationTaskFactory;
import org.briarproject.api.lifecycle.LifecycleManager;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.widget.Toast;
@@ -56,9 +45,6 @@ implements InvitationListener {
private InvitationTask task = null;
private long taskHandle = -1;
private AuthorId localAuthorId = null;
private String networkName = null;
private boolean bluetoothEnabled = false;
private BluetoothWifiStateReceiver receiver = null;
private int localInvitationCode = -1, remoteInvitationCode = -1;
private int localConfirmationCode = -1, remoteConfirmationCode = -1;
private boolean connected = false, connectionFailed = false;
@@ -76,7 +62,7 @@ implements InvitationListener {
super.onCreate(state);
if(state == null) {
// This is a new activity
setView(new NetworkSetupView(this));
setView(new ChooseIdentityView(this));
} else {
// Restore the activity's state
byte[] b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
@@ -96,7 +82,7 @@ implements InvitationListener {
}
// Set the appropriate view for the state
if(localInvitationCode == -1) {
setView(new NetworkSetupView(this));
setView(new ChooseIdentityView(this));
} else if(remoteInvitationCode == -1) {
setView(new InvitationCodeView(this));
} else if(connectionFailed) {
@@ -123,7 +109,7 @@ implements InvitationListener {
contactName = s.getContactName();
// Set the appropriate view for the state
if(localInvitationCode == -1) {
setView(new NetworkSetupView(this));
setView(new ChooseIdentityView(this));
} else if(remoteInvitationCode == -1) {
setView(new InvitationCodeView(this));
} else if(connectionFailed) {
@@ -148,25 +134,6 @@ implements InvitationListener {
}
}
}
// Listen for Bluetooth and WiFi state changes
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
filter.addAction(ACTION_SCAN_MODE_CHANGED);
filter.addAction(NETWORK_STATE_CHANGED_ACTION);
receiver = new BluetoothWifiStateReceiver();
registerReceiver(receiver, filter);
// Get the current Bluetooth and WiFi state
BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
if(bluetooth != null) bluetoothEnabled = bluetooth.isEnabled();
view.bluetoothStateChanged();
WifiManager wifi = (WifiManager) getSystemService(WIFI_SERVICE);
if(wifi != null && wifi.isWifiEnabled()) {
WifiInfo info = wifi.getConnectionInfo();
if(info.getNetworkId() != -1) networkName = info.getSSID();
}
view.wifiStateChanged();
}
private void showToastAndFinish() {
@@ -198,7 +165,13 @@ implements InvitationListener {
public void onDestroy() {
super.onDestroy();
if(task != null) task.removeListener(this);
if(receiver != null) unregisterReceiver(receiver);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
// This is the result of enabling Bluetooth
if(result != RESULT_CANCELED)
reset(new InvitationCodeView(this));
}
void setView(AddContactView view) {
@@ -208,7 +181,7 @@ implements InvitationListener {
}
void reset(AddContactView view) {
// Don't reset localAuthorId, networkName or bluetoothEnabled
// Don't reset localAuthorId
task = null;
taskHandle = -1;
localInvitationCode = -1;
@@ -264,23 +237,20 @@ implements InvitationListener {
return localAuthorId;
}
String getNetworkName() {
return networkName;
}
boolean isBluetoothEnabled() {
return bluetoothEnabled;
}
int getLocalInvitationCode() {
if(localInvitationCode == -1)
localInvitationCode = crypto.generateInvitationCode();
return localInvitationCode;
}
int getRemoteInvitationCode() {
return remoteInvitationCode;
}
void remoteInvitationCodeEntered(int code) {
if(localAuthorId == null) throw new IllegalStateException();
if(localInvitationCode == -1) throw new IllegalStateException();
remoteInvitationCode = code;
setView(new ConnectionView(this));
task = invitationTaskFactory.createTask(localAuthorId,
localInvitationCode, code);
@@ -392,30 +362,6 @@ implements InvitationListener {
});
}
private class BluetoothWifiStateReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent intent) {
String action = intent.getAction();
if(action.equals(ACTION_STATE_CHANGED)) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
bluetoothEnabled = state == STATE_ON;
view.bluetoothStateChanged();
} else if(action.equals(ACTION_SCAN_MODE_CHANGED)) {
view.bluetoothStateChanged();
} else if(action.equals(NETWORK_STATE_CHANGED_ACTION)) {
WifiManager wifi = (WifiManager) getSystemService(WIFI_SERVICE);
if(wifi == null || !wifi.isWifiEnabled()) {
networkName = null;
} else {
WifiInfo info = wifi.getConnectionInfo();
if(info.getNetworkId() == -1) networkName = null;
else networkName = info.getSSID();
}
view.wifiStateChanged();
}
}
}
/**
* Cleans up the reference to the invitation task when the task completes.
* This class is static to prevent memory leaks.

View File

@@ -28,8 +28,4 @@ abstract class AddContactView extends LinearLayout {
}
abstract void populate();
void wifiStateChanged() {}
void bluetoothStateChanged() {}
}

View File

@@ -1,90 +0,0 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class BluetoothStatusView extends LinearLayout implements OnClickListener {
private final int pad;
public BluetoothStatusView(Context ctx) {
super(ctx);
pad = LayoutUtils.getPadding(ctx);
}
void init() {
setOrientation(HORIZONTAL);
setGravity(CENTER);
populate();
}
void populate() {
removeAllViews();
Context ctx = getContext();
TextView status = new TextView(ctx);
status.setLayoutParams(WRAP_WRAP_1);
status.setTextSize(14);
status.setPadding(pad, pad, pad, pad);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if(adapter == null) {
ImageView warning = new ImageView(ctx);
warning.setPadding(pad, pad, pad, pad);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.bluetooth_not_available);
addView(status);
} else if(adapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
ImageView ok = new ImageView(ctx);
ok.setPadding(pad, pad, pad, pad);
ok.setImageResource(R.drawable.navigation_accept);
addView(ok);
status.setText(R.string.bluetooth_discoverable);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
} else if(adapter.isEnabled()) {
ImageView warning = new ImageView(ctx);
warning.setPadding(pad, pad, pad, pad);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.bluetooth_not_discoverable);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
} else {
ImageView warning = new ImageView(ctx);
warning.setPadding(pad, pad, pad, pad);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.bluetooth_disabled);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
}
}
public void onClick(View view) {
getContext().startActivity(new Intent(ACTION_BLUETOOTH_SETTINGS));
}
}

View File

@@ -1,5 +1,7 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.identity.LocalAuthorItem.NEW;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
@@ -9,7 +11,6 @@ import org.briarproject.R;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.api.AuthorId;
import android.content.Context;
import android.content.Intent;
@@ -22,16 +23,14 @@ import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
class NetworkSetupView extends AddContactView
class ChooseIdentityView extends AddContactView
implements OnItemSelectedListener, OnClickListener {
private LocalAuthorSpinnerAdapter adapter = null;
private Spinner spinner = null;
private WifiStatusView wifi = null;
private BluetoothStatusView bluetooth = null;
private Button continueButton = null;
NetworkSetupView(Context ctx) {
ChooseIdentityView(Context ctx) {
super(ctx);
}
@@ -58,48 +57,19 @@ implements OnItemSelectedListener, OnClickListener {
innerLayout.addView(spinner);
addView(innerLayout);
wifi = new WifiStatusView(ctx);
wifi.init();
addView(wifi);
bluetooth = new BluetoothStatusView(ctx);
bluetooth.init();
addView(bluetooth);
TextView faceToFace = new TextView(ctx);
faceToFace.setGravity(CENTER);
faceToFace.setTextSize(14);
faceToFace.setPadding(pad, pad, pad, pad);
faceToFace.setText(R.string.fact_to_face);
faceToFace.setText(R.string.face_to_face);
addView(faceToFace);
continueButton = new Button(ctx);
continueButton.setLayoutParams(WRAP_WRAP);
continueButton.setText(R.string.continue_button);
continueButton.setOnClickListener(this);
enableOrDisableContinueButton();
addView(continueButton);
}
void wifiStateChanged() {
if(wifi != null) wifi.populate();
enableOrDisableContinueButton();
}
void bluetoothStateChanged() {
if(bluetooth != null) bluetooth.populate();
enableOrDisableContinueButton();
}
private void enableOrDisableContinueButton() {
if(continueButton == null) return; // Activity not created yet
AuthorId localAuthorId = container.getLocalAuthorId();
boolean bluetoothEnabled = container.isBluetoothEnabled();
String networkName = container.getNetworkName();
boolean networkAvailable = bluetoothEnabled || networkName != null;
continueButton.setEnabled(localAuthorId != null && networkAvailable);
}
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
LocalAuthorItem item = adapter.getItem(position);
@@ -110,7 +80,6 @@ implements OnItemSelectedListener, OnClickListener {
} else {
container.setLocalAuthorId(item.getLocalAuthor().getId());
}
enableOrDisableContinueButton();
}
public void onNothingSelected(AdapterView<?> parent) {
@@ -118,6 +87,8 @@ implements OnItemSelectedListener, OnClickListener {
}
public void onClick(View view) {
container.setView(new InvitationCodeView(container));
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
container.startActivityForResult(i, 0);
}
}

View File

@@ -1,11 +1,14 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import org.briarproject.R;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -38,6 +41,7 @@ class CodesDoNotMatchView extends AddContactView implements OnClickListener {
addView(innerLayout);
TextView interfering = new TextView(ctx);
interfering.setGravity(CENTER);
interfering.setTextSize(14);
interfering.setPadding(pad, 0, pad, pad);
interfering.setText(R.string.interfering);
@@ -51,7 +55,8 @@ class CodesDoNotMatchView extends AddContactView implements OnClickListener {
}
public void onClick(View view) {
// Try again
container.reset(new NetworkSetupView(container));
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
container.startActivityForResult(i, 0);
}
}

View File

@@ -1,11 +1,14 @@
package org.briarproject.android.invitation;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import org.briarproject.R;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -15,8 +18,6 @@ import android.widget.TextView;
class ConnectionFailedView extends AddContactView implements OnClickListener {
private WifiStatusView wifi = null;
private BluetoothStatusView bluetooth = null;
private Button tryAgainButton = null;
ConnectionFailedView(Context ctx) {
@@ -41,47 +42,23 @@ class ConnectionFailedView extends AddContactView implements OnClickListener {
innerLayout.addView(failed);
addView(innerLayout);
TextView checkNetwork = new TextView(ctx);
checkNetwork.setTextSize(14);
checkNetwork.setPadding(pad, 0, pad, pad);
checkNetwork.setText(R.string.check_same_network);
addView(checkNetwork);
wifi = new WifiStatusView(ctx);
wifi.init();
addView(wifi);
bluetooth = new BluetoothStatusView(ctx);
bluetooth.init();
addView(bluetooth);
TextView couldNotFind = new TextView(ctx);
couldNotFind.setGravity(CENTER);
couldNotFind.setTextSize(14);
couldNotFind.setPadding(pad, 0, pad, pad);
couldNotFind.setText(R.string.could_not_find_contact);
addView(couldNotFind);
tryAgainButton = new Button(ctx);
tryAgainButton.setLayoutParams(WRAP_WRAP);
tryAgainButton.setText(R.string.try_again_button);
tryAgainButton.setOnClickListener(this);
enableOrDisableTryAgainButton();
addView(tryAgainButton);
}
void wifiStateChanged() {
if(wifi != null) wifi.populate();
enableOrDisableTryAgainButton();
}
void bluetoothStateChanged() {
if(bluetooth != null) bluetooth.populate();
enableOrDisableTryAgainButton();
}
private void enableOrDisableTryAgainButton() {
if(tryAgainButton == null) return; // Activity not created yet
boolean bluetoothEnabled = container.isBluetoothEnabled();
String networkName = container.getNetworkName();
tryAgainButton.setEnabled(bluetoothEnabled || networkName != null);
}
public void onClick(View view) {
// Try again
container.reset(new InvitationCodeView(container));
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
container.startActivityForResult(i, 0);
}
}

View File

@@ -34,41 +34,21 @@ class ConnectionView extends AddContactView {
code.setText(String.format("%06d", localCode));
addView(code);
String networkName = container.getNetworkName();
if(networkName != null) {
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setIndeterminate(true);
progress.setPadding(pad, pad, pad, pad);
innerLayout.addView(progress);
ProgressBar progress = new ProgressBar(ctx);
progress.setPadding(pad, pad, pad, pad);
progress.setIndeterminate(true);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
String format = getResources().getString(
R.string.format_connecting_wifi);
connecting.setText(String.format(format, networkName));
innerLayout.addView(connecting);
TextView connecting = new TextView(ctx);
int remoteCode = container.getRemoteInvitationCode();
String format = ctx.getResources().getString(R.string.searching_format);
connecting.setText(String.format(format, remoteCode));
innerLayout.addView(connecting);
addView(innerLayout);
}
if(container.isBluetoothEnabled()) {
LinearLayout innerLayout = new LinearLayout(ctx);
innerLayout.setOrientation(HORIZONTAL);
innerLayout.setGravity(CENTER);
ProgressBar progress = new ProgressBar(ctx);
progress.setPadding(pad, pad, pad, pad);
progress.setIndeterminate(true);
innerLayout.addView(progress);
TextView connecting = new TextView(ctx);
connecting.setText(R.string.connecting_bluetooth);
innerLayout.addView(connecting);
addView(innerLayout);
}
addView(innerLayout);
}
}

View File

@@ -1,98 +0,0 @@
package org.briarproject.android.invitation;
import static android.content.Context.WIFI_SERVICE;
import static android.provider.Settings.ACTION_WIFI_SETTINGS;
import static android.view.Gravity.CENTER;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class WifiStatusView extends LinearLayout implements OnClickListener {
private final int pad;
public WifiStatusView(Context ctx) {
super(ctx);
pad = LayoutUtils.getPadding(ctx);
}
void init() {
setOrientation(HORIZONTAL);
setGravity(CENTER);
populate();
}
void populate() {
removeAllViews();
Context ctx = getContext();
TextView status = new TextView(ctx);
status.setTextSize(14);
status.setPadding(pad, pad, pad, pad);
status.setLayoutParams(WRAP_WRAP_1);
WifiManager wifi = (WifiManager) ctx.getSystemService(WIFI_SERVICE);
if(wifi == null) {
ImageView warning = new ImageView(ctx);
warning.setPadding(pad, pad, pad, pad);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.wifi_not_available);
addView(status);
} else if(wifi.isWifiEnabled()) {
WifiInfo info = wifi.getConnectionInfo();
String networkName = info.getSSID();
int networkId = info.getNetworkId();
if(networkName == null || networkId == -1) {
ImageView warning = new ImageView(ctx);
warning.setPadding(pad, pad, pad, pad);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.wifi_disconnected);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
} else {
ImageView ok = new ImageView(ctx);
ok.setPadding(pad, pad, pad, pad);
ok.setImageResource(R.drawable.navigation_accept);
addView(ok);
String format = getResources().getString(
R.string.format_wifi_connected);
status.setText(String.format(format, networkName));
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
}
} else {
ImageView warning = new ImageView(ctx);
warning.setPadding(pad, pad, pad, pad);
warning.setImageResource(R.drawable.alerts_and_states_warning);
addView(warning);
status.setText(R.string.wifi_disabled);
addView(status);
ImageButton settings = new ImageButton(ctx);
settings.setImageResource(R.drawable.action_settings);
settings.setOnClickListener(this);
addView(settings);
}
}
public void onClick(View view) {
getContext().startActivity(new Intent(ACTION_WIFI_SETTINGS));
}
}

View File

@@ -14,7 +14,7 @@ import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
import org.briarproject.plugins.tcp.DroidLanTcpPluginFactory;
import org.briarproject.plugins.tcp.LanTcpPluginFactory;
import org.briarproject.plugins.tor.TorPluginFactory;
import android.content.Context;
@@ -45,8 +45,7 @@ public class AndroidPluginsModule extends AbstractModule {
crypto.getSecureRandom());
DuplexPluginFactory tor = new TorPluginFactory(pluginExecutor,
appContext, shutdownManager);
DuplexPluginFactory lan = new DroidLanTcpPluginFactory(pluginExecutor,
appContext);
DuplexPluginFactory lan = new LanTcpPluginFactory(pluginExecutor);
final Collection<DuplexPluginFactory> factories =
Arrays.asList(droidtooth, tor, lan);
return new DuplexPluginConfig() {

View File

@@ -1,42 +0,0 @@
package org.briarproject.plugins.tcp;
import static android.content.Context.WIFI_SERVICE;
import java.util.concurrent.Executor;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.system.Clock;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
class DroidLanTcpPlugin extends LanTcpPlugin {
private final Context appContext;
DroidLanTcpPlugin(Executor pluginExecutor, Context appContext, Clock clock,
DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
long pollingInterval) {
super(pluginExecutor, clock, callback, maxFrameLength, maxLatency,
pollingInterval);
this.appContext = appContext;
}
@Override
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) {
Object o = appContext.getSystemService(WIFI_SERVICE);
WifiManager wifi = (WifiManager) o;
if(wifi == null || !wifi.isWifiEnabled()) return null;
MulticastLock lock = wifi.createMulticastLock("invitation");
lock.acquire();
try {
return super.createInvitationConnection(r, timeout);
} finally {
lock.release();
}
}
}

View File

@@ -1,39 +0,0 @@
package org.briarproject.plugins.tcp;
import java.util.concurrent.Executor;
import org.briarproject.api.TransportId;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.system.Clock;
import org.briarproject.system.SystemClock;
import android.content.Context;
public class DroidLanTcpPluginFactory implements DuplexPluginFactory {
private static final int MAX_FRAME_LENGTH = 1024;
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
private static final long POLLING_INTERVAL = 60 * 1000; // 1 minute
private final Executor pluginExecutor;
private final Context appContext;
private final Clock clock;
public DroidLanTcpPluginFactory(Executor pluginExecutor,
Context appContext) {
this.pluginExecutor = pluginExecutor;
this.appContext = appContext;
clock = new SystemClock();
}
public TransportId getId() {
return LanTcpPlugin.ID;
}
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
return new DroidLanTcpPlugin(pluginExecutor, appContext, clock,
callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
}
}

View File

@@ -1,17 +1,11 @@
package org.briarproject.plugins.tcp;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
@@ -23,12 +17,7 @@ import java.util.logging.Logger;
import org.briarproject.api.TransportId;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.system.Clock;
import org.briarproject.util.ByteUtils;
import org.briarproject.util.LatchedReference;
import org.briarproject.util.StringUtils;
/** A socket plugin that supports exchanging invitations over a LAN. */
@@ -38,16 +27,11 @@ class LanTcpPlugin extends TcpPlugin {
private static final Logger LOG =
Logger.getLogger(LanTcpPlugin.class.getName());
private static final int MULTICAST_INTERVAL = 1000; // 1 second
private final Clock clock;
LanTcpPlugin(Executor pluginExecutor, Clock clock,
DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
long pollingInterval) {
LanTcpPlugin(Executor pluginExecutor, DuplexPluginCallback callback,
int maxFrameLength, long maxLatency, long pollingInterval) {
super(pluginExecutor, callback, maxFrameLength, maxLatency,
pollingInterval);
this.clock = clock;
}
public TransportId getId() {
@@ -106,232 +90,4 @@ class LanTcpPlugin extends TcpPlugin {
}
return addrs;
}
public boolean supportsInvitations() {
return true;
}
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) {
if(!running) return null;
// Use the invitation codes to generate the group address and port
InetSocketAddress group = chooseMulticastGroup(r);
// Bind a multicast socket for sending and receiving packets
InetAddress iface = null;
MulticastSocket ms = null;
try {
iface = chooseInvitationInterface();
if(iface == null) return null;
ms = new MulticastSocket(group.getPort());
ms.setInterface(iface);
ms.joinGroup(group.getAddress());
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
if(ms != null) tryToClose(ms, group.getAddress());
return null;
}
// Bind a server socket for receiving invitation connections
ServerSocket ss = null;
try {
ss = new ServerSocket();
ss.bind(new InetSocketAddress(iface, 0));
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
if(ss != null) tryToClose(ss);
return null;
}
// Start the listener threads
LatchedReference<Socket> socketLatch = new LatchedReference<Socket>();
new MulticastListenerThread(socketLatch, ms, iface).start();
new TcpListenerThread(socketLatch, ss).start();
// Send packets until a connection is made or we run out of time
byte[] buffer = new byte[2];
ByteUtils.writeUint16(ss.getLocalPort(), buffer, 0);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
packet.setAddress(group.getAddress());
packet.setPort(group.getPort());
long now = clock.currentTimeMillis();
long end = now + timeout;
try {
while(now < end && running) {
// Send a packet
if(LOG.isLoggable(INFO)) LOG.info("Sending multicast packet");
ms.send(packet);
// Wait for an incoming or outgoing connection
try {
Socket s = socketLatch.waitForReference(MULTICAST_INTERVAL);
if(s != null) return new TcpTransportConnection(this, s);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while exchanging invitations");
Thread.currentThread().interrupt();
return null;
}
now = clock.currentTimeMillis();
}
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
} finally {
// Closing the sockets will terminate the listener threads
tryToClose(ms, group.getAddress());
tryToClose(ss);
}
return null;
}
private InetSocketAddress chooseMulticastGroup(PseudoRandom r) {
byte[] b = r.nextBytes(5);
// The group address is 239.random.random.random, excluding 0 and 255
byte[] group = new byte[4];
group[0] = (byte) 239;
group[1] = legalAddressByte(b[0]);
group[2] = legalAddressByte(b[1]);
group[3] = legalAddressByte(b[2]);
// The port is random in the range 32768 - 65535, inclusive
int port = ByteUtils.readUint16(b, 3);
if(port < 32768) port += 32768;
InetAddress address;
try {
address = InetAddress.getByAddress(group);
} catch(UnknownHostException badAddressLength) {
throw new RuntimeException(badAddressLength);
}
return new InetSocketAddress(address, port);
}
private byte legalAddressByte(byte b) {
if(b == 0) return 1;
if(b == (byte) 255) return (byte) 254;
return b;
}
private InetAddress chooseInvitationInterface() throws IOException {
List<NetworkInterface> ifaces =
Collections.list(NetworkInterface.getNetworkInterfaces());
// Prefer an interface with a link-local or site-local address
for(NetworkInterface iface : ifaces) {
for(InetAddress addr : Collections.list(iface.getInetAddresses())) {
if(addr.isLoopbackAddress()) continue;
boolean link = addr.isLinkLocalAddress();
boolean site = addr.isSiteLocalAddress();
if(link || site) return addr;
}
}
// Accept an interface without a link-local or site-local address
for(NetworkInterface iface : ifaces) {
for(InetAddress addr : Collections.list(iface.getInetAddresses())) {
if(!addr.isLoopbackAddress()) return addr;
}
}
// No suitable interfaces
return null;
}
private void tryToClose(MulticastSocket ms, InetAddress addr) {
try {
ms.leaveGroup(addr);
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
ms.close();
}
private class MulticastListenerThread extends Thread {
private final LatchedReference<Socket> socketLatch;
private final MulticastSocket multicastSocket;
private final InetAddress localAddress;
private MulticastListenerThread(LatchedReference<Socket> socketLatch,
MulticastSocket multicastSocket, InetAddress localAddress) {
this.socketLatch = socketLatch;
this.multicastSocket = multicastSocket;
this.localAddress = localAddress;
}
@Override
public void run() {
if(LOG.isLoggable(INFO))
LOG.info("Listening for multicast packets");
// Listen until a valid packet is received or the socket is closed
byte[] buffer = new byte[2];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
while(running) {
multicastSocket.receive(packet);
if(LOG.isLoggable(INFO))
LOG.info("Received multicast packet");
parseAndConnectBack(packet);
}
} catch(IOException e) {
// This is expected when the socket is closed
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
}
}
private void parseAndConnectBack(DatagramPacket packet) {
InetAddress addr = packet.getAddress();
if(addr.equals(localAddress)) {
if(LOG.isLoggable(INFO)) LOG.info("Ignoring own packet");
return;
}
byte[] b = packet.getData();
int off = packet.getOffset();
int len = packet.getLength();
if(len != 2) {
if(LOG.isLoggable(INFO)) LOG.info("Invalid length: " + len);
return;
}
int port = ByteUtils.readUint16(b, off);
if(port < 32768 || port >= 65536) {
if(LOG.isLoggable(INFO)) LOG.info("Invalid port: " + port);
return;
}
if(LOG.isLoggable(INFO))
LOG.info("Packet from " + getHostAddress(addr) + ":" + port);
try {
// Connect back on the advertised TCP port
Socket s = new Socket(addr, port);
if(LOG.isLoggable(INFO)) LOG.info("Outgoing connection");
if(!socketLatch.set(s)) {
if(LOG.isLoggable(INFO))
LOG.info("Closing redundant connection");
s.close();
}
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}
private class TcpListenerThread extends Thread {
private final LatchedReference<Socket> socketLatch;
private final ServerSocket serverSocket;
private TcpListenerThread(LatchedReference<Socket> socketLatch,
ServerSocket serverSocket) {
this.socketLatch = socketLatch;
this.serverSocket = serverSocket;
}
@Override
public void run() {
if(LOG.isLoggable(INFO))
LOG.info("Listening for invitation connections");
// Listen until a connection is received or the socket is closed
try {
Socket s = serverSocket.accept();
if(LOG.isLoggable(INFO)) LOG.info("Incoming connection");
if(!socketLatch.set(s)) {
if(LOG.isLoggable(INFO))
LOG.info("Closing redundant connection");
s.close();
}
} catch(IOException e) {
// This is expected when the socket is closed
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
}
}
}
}

View File

@@ -6,8 +6,6 @@ import org.briarproject.api.TransportId;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.system.Clock;
import org.briarproject.system.SystemClock;
public class LanTcpPluginFactory implements DuplexPluginFactory {
@@ -16,11 +14,9 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
private static final long POLLING_INTERVAL = 60 * 1000; // 1 minute
private final Executor pluginExecutor;
private final Clock clock;
public LanTcpPluginFactory(Executor pluginExecutor) {
this.pluginExecutor = pluginExecutor;
clock = new SystemClock();
}
public TransportId getId() {
@@ -28,7 +24,7 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
}
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
return new LanTcpPlugin(pluginExecutor, clock, callback,
MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
return new LanTcpPlugin(pluginExecutor, callback, MAX_FRAME_LENGTH,
MAX_LATENCY, POLLING_INTERVAL);
}
}

View File

@@ -18,6 +18,7 @@ import java.util.logging.Logger;
import org.briarproject.api.ContactId;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
@@ -207,4 +208,13 @@ abstract class TcpPlugin implements DuplexPlugin {
return null;
}
}
public boolean supportsInvitations() {
return false;
}
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) {
throw new UnsupportedOperationException();
}
}

View File

@@ -17,9 +17,7 @@ import java.util.logging.Logger;
import org.briarproject.api.TransportId;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.util.StringUtils;
class WanTcpPlugin extends TcpPlugin {
@@ -112,13 +110,4 @@ class WanTcpPlugin extends TcpPlugin {
p.put("port", String.valueOf(a.getPort()));
callback.mergeLocalProperties(p);
}
public boolean supportsInvitations() {
return false;
}
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) {
throw new UnsupportedOperationException();
}
}

View File

@@ -9,9 +9,7 @@ import java.util.concurrent.Executors;
import org.briarproject.api.ContactId;
import org.briarproject.api.TransportConfig;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.system.Clock;
import org.briarproject.plugins.DuplexClientTest;
import org.briarproject.system.SystemClock;
// This is not a JUnit test - it has to be run manually while the server test
// is running on another machine
@@ -21,15 +19,14 @@ public class LanTcpClientTest extends DuplexClientTest {
String serverPort) {
// Store the server's internal address and port
TransportProperties p = new TransportProperties();
p.put("internal", serverAddress);
p.put("address", serverAddress);
p.put("port", serverPort);
Map<ContactId, TransportProperties> remote =
Collections.singletonMap(contactId, p);
// Create the plugin
callback = new ClientCallback(new TransportConfig(),
new TransportProperties(), remote);
Clock clock = new SystemClock();
plugin = new LanTcpPlugin(executor, clock, callback, 0, 0, 0);
plugin = new LanTcpPlugin(executor, callback, 0, 0, 0);
}
public static void main(String[] args) throws Exception {

View File

@@ -20,9 +20,6 @@ import org.briarproject.api.TransportProperties;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.system.Clock;
import org.briarproject.system.SystemClock;
import org.junit.Test;
public class LanTcpPluginTest extends BriarTestCase {
@@ -35,9 +32,7 @@ public class LanTcpPluginTest extends BriarTestCase {
callback.local.put("address", "127.0.0.1");
callback.local.put("port", "0");
Executor executor = Executors.newCachedThreadPool();
Clock clock = new SystemClock();
DuplexPlugin plugin =
new LanTcpPlugin(executor, clock, callback, 0, 0, 0);
DuplexPlugin plugin = new LanTcpPlugin(executor, callback, 0, 0, 0);
plugin.start();
// The plugin should have bound a socket and stored the port number
assertTrue(callback.propertiesLatch.await(5, SECONDS));
@@ -62,9 +57,7 @@ public class LanTcpPluginTest extends BriarTestCase {
public void testOutgoingConnection() throws Exception {
Callback callback = new Callback();
Executor executor = Executors.newCachedThreadPool();
Clock clock = new SystemClock();
DuplexPlugin plugin =
new LanTcpPlugin(executor, clock, callback, 0, 0, 0);
DuplexPlugin plugin = new LanTcpPlugin(executor, callback, 0, 0, 0);
plugin.start();
// Listen on a local port
final ServerSocket ss = new ServerSocket();

View File

@@ -7,9 +7,7 @@ import java.util.concurrent.Executors;
import org.briarproject.api.TransportConfig;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.system.Clock;
import org.briarproject.plugins.DuplexServerTest;
import org.briarproject.system.SystemClock;
// This is not a JUnit test - it has to be run manually while the client test
// is running on another machine
@@ -19,8 +17,7 @@ public class LanTcpServerTest extends DuplexServerTest {
callback = new ServerCallback(new TransportConfig(),
new TransportProperties(),
Collections.singletonMap(contactId, new TransportProperties()));
Clock clock = new SystemClock();
plugin = new LanTcpPlugin(executor, clock, callback, 0, 0, 0);
plugin = new LanTcpPlugin(executor, callback, 0, 0, 0);
}
public static void main(String[] args) throws Exception {