diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e9dfd8090..799292896 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3,11 +3,23 @@
package="net.sf.briar"
android:versionCode="1"
android:versionName="1.0" >
+
+
-
+
+
+
+
+
+
+
+
+
@@ -16,10 +28,37 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..1b63e455b
Binary files /dev/null and b/res/drawable-hdpi/ic_launcher.png differ
diff --git a/res/drawable-ldpi/ic_launcher.png b/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 000000000..e0119f086
Binary files /dev/null and b/res/drawable-ldpi/ic_launcher.png differ
diff --git a/res/drawable-ldpi/iconic_check_alt_green.png b/res/drawable-ldpi/iconic_check_alt_green.png
new file mode 100644
index 000000000..0751e8d49
Binary files /dev/null and b/res/drawable-ldpi/iconic_check_alt_green.png differ
diff --git a/res/drawable-ldpi/iconic_x_alt_red.png b/res/drawable-ldpi/iconic_x_alt_red.png
new file mode 100644
index 000000000..d560b2646
Binary files /dev/null and b/res/drawable-ldpi/iconic_x_alt_red.png differ
diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..9873a76f0
Binary files /dev/null and b/res/drawable-mdpi/ic_launcher.png differ
diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..0b22fd5d7
Binary files /dev/null and b/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/res/layout/activity_add_contact.xml b/res/layout/activity_add_contact.xml
new file mode 100644
index 000000000..c37b80fec
--- /dev/null
+++ b/res/layout/activity_add_contact.xml
@@ -0,0 +1,5 @@
+
diff --git a/res/layout/activity_codes_do_not_match.xml b/res/layout/activity_codes_do_not_match.xml
new file mode 100644
index 000000000..4bd1965bc
--- /dev/null
+++ b/res/layout/activity_codes_do_not_match.xml
@@ -0,0 +1,5 @@
+
diff --git a/res/layout/activity_connection.xml b/res/layout/activity_connection.xml
new file mode 100644
index 000000000..367b75fc5
--- /dev/null
+++ b/res/layout/activity_connection.xml
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/layout/activity_connection_failed.xml b/res/layout/activity_connection_failed.xml
new file mode 100644
index 000000000..9672914f4
--- /dev/null
+++ b/res/layout/activity_connection_failed.xml
@@ -0,0 +1,5 @@
+
diff --git a/res/layout/activity_connection_succeeded.xml b/res/layout/activity_connection_succeeded.xml
new file mode 100644
index 000000000..0b6962125
--- /dev/null
+++ b/res/layout/activity_connection_succeeded.xml
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/layout/activity_contact_added.xml b/res/layout/activity_contact_added.xml
new file mode 100644
index 000000000..05a3c684f
--- /dev/null
+++ b/res/layout/activity_contact_added.xml
@@ -0,0 +1,5 @@
+
diff --git a/res/layout/activity_invitation_code.xml b/res/layout/activity_invitation_code.xml
new file mode 100644
index 000000000..193e86aaf
--- /dev/null
+++ b/res/layout/activity_invitation_code.xml
@@ -0,0 +1,5 @@
+
diff --git a/res/layout/activity_network_setup.xml b/res/layout/activity_network_setup.xml
new file mode 100644
index 000000000..48d2de513
--- /dev/null
+++ b/res/layout/activity_network_setup.xml
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/layout/activity_test_bluetooth.xml b/res/layout/activity_test_bluetooth.xml
new file mode 100644
index 000000000..eaf4032a4
--- /dev/null
+++ b/res/layout/activity_test_bluetooth.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/activity_wait_for_contact.xml b/res/layout/activity_wait_for_contact.xml
new file mode 100644
index 000000000..cd96d411b
--- /dev/null
+++ b/res/layout/activity_wait_for_contact.xml
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eeabdb397..0a2f6313d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,4 +1,40 @@
Briar
+ Settings
+ Add a Contact
+ Welcome to Briar! Add a contact to get started.
+ Add a contact
+ For security reasons you must be face to face with someone to add them as a contact.
+ Briar can add contacts via Wi-Fi or Bluetooth. To use Wi-Fi you must both be connected to the same network.
+ Wi-Fi is not available on this device.
+ Wi-Fi is OFF.
+ Turn on Wi-Fi
+ Wi-Fi is DISCONNECTED.
+ Connect to Wi-Fi
+ Wi-Fi is CONNECTED to %1$s.
+ Bluetooth is not available on this device.
+ Bluetooth is OFF.
+ Turn on Bluetooth
+ Bluetooth is NOT DISCOVERABLE.
+ Make Bluetooth discoverable
+ Bluetooth is ON.
+ Continue
+ Your invitation code is
+ Please enter your contact\'s invitation code:
+ Connecting via %1$s\u2026
+ Connecting via Bluetooth\u2026
+ Connection failed.
+ Please check that you are both using the same network.
+ Try again
+ Connected to contact.
+ Your confirmation code is
+ Please enter your contact\'s confirmation code:
+ Waiting for contact\u2026
+ Codes do not match!
+ This could mean that someone is trying to interfere with your connection.
+ Contact added.
+ Please enter a nickname for this contact:
+ Add another contact
+ Done
diff --git a/src/net/sf/briar/HelloWorldActivity.java b/src/net/sf/briar/HelloWorldActivity.java
index 53607532d..76868cb80 100644
--- a/src/net/sf/briar/HelloWorldActivity.java
+++ b/src/net/sf/briar/HelloWorldActivity.java
@@ -1,19 +1,41 @@
package net.sf.briar;
+import net.sf.briar.android.invitation.NetworkSetupActivity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
import android.widget.TextView;
-public class HelloWorldActivity extends Activity {
+public class HelloWorldActivity extends Activity implements OnClickListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- TextView text = new TextView(this);
- text.setText("Hello world");
- setContentView(text);
+ setContentView(R.layout.activity_add_contact);
+ LinearLayout layout = (LinearLayout) findViewById(
+ R.id.add_contact_container);
+
+ TextView welcome = new TextView(this);
+ welcome.setText(R.string.welcome);
+ layout.addView(welcome);
+ Button addContact = new Button(this);
+ addContact.setText(R.string.add_contact_button);
+ addContact.setOnClickListener(this);
+ layout.addView(addContact);
+ TextView faceToFace = new TextView(this);
+ faceToFace.setText(R.string.face_to_face);
+ layout.addView(faceToFace);
+
Intent intent = new Intent("net.sf.briar.HelloWorldService");
startService(intent);
}
+
+ public void onClick(View view) {
+ startActivity(new Intent(this, NetworkSetupActivity.class));
+ finish();
+ }
}
diff --git a/src/net/sf/briar/android/invitation/BluetoothStateListener.java b/src/net/sf/briar/android/invitation/BluetoothStateListener.java
new file mode 100644
index 000000000..fa7846c7c
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/BluetoothStateListener.java
@@ -0,0 +1,6 @@
+package net.sf.briar.android.invitation;
+
+interface BluetoothStateListener {
+
+ void bluetoothStateChanged(boolean enabled);
+}
diff --git a/src/net/sf/briar/android/invitation/BluetoothWidget.java b/src/net/sf/briar/android/invitation/BluetoothWidget.java
new file mode 100644
index 000000000..e97d7d86e
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/BluetoothWidget.java
@@ -0,0 +1,71 @@
+package net.sf.briar.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_HORIZONTAL;
+import net.sf.briar.R;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.content.Intent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class BluetoothWidget extends LinearLayout implements OnClickListener {
+
+ private BluetoothStateListener listener = null;
+
+ public BluetoothWidget(Context ctx) {
+ super(ctx);
+ }
+
+ void init(BluetoothStateListener listener) {
+ this.listener = listener;
+ setOrientation(VERTICAL);
+ setPadding(0, 10, 0, 10);
+ populate();
+ }
+
+ void populate() {
+ removeAllViews();
+ Context ctx = getContext();
+ TextView status = new TextView(ctx);
+ status.setGravity(CENTER_HORIZONTAL);
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if(adapter == null) {
+ bluetoothStateChanged(false);
+ status.setText(R.string.bluetooth_not_available);
+ addView(status);
+ } else if(adapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ bluetoothStateChanged(true);
+ status.setText(R.string.bluetooth_enabled);
+ addView(status);
+ } else if(adapter.isEnabled()) {
+ bluetoothStateChanged(false);
+ status.setText(R.string.bluetooth_not_discoverable);
+ addView(status);
+ Button turnOn = new Button(ctx);
+ turnOn.setText(R.string.make_bluetooth_discoverable_button);
+ turnOn.setOnClickListener(this);
+ addView(turnOn);
+ } else {
+ bluetoothStateChanged(false);
+ status.setText(R.string.bluetooth_disabled);
+ addView(status);
+ Button turnOn = new Button(ctx);
+ turnOn.setText(R.string.turn_on_bluetooth_button);
+ turnOn.setOnClickListener(this);
+ addView(turnOn);
+ }
+ }
+
+ private void bluetoothStateChanged(boolean enabled) {
+ listener.bluetoothStateChanged(enabled);
+ }
+
+ public void onClick(View view) {
+ getContext().startActivity(new Intent(ACTION_BLUETOOTH_SETTINGS));
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/CodeEntryListener.java b/src/net/sf/briar/android/invitation/CodeEntryListener.java
new file mode 100644
index 000000000..1a8f7b3d8
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/CodeEntryListener.java
@@ -0,0 +1,6 @@
+package net.sf.briar.android.invitation;
+
+interface CodeEntryListener {
+
+ void codeEntered(String code);
+}
diff --git a/src/net/sf/briar/android/invitation/CodeEntryWidget.java b/src/net/sf/briar/android/invitation/CodeEntryWidget.java
new file mode 100644
index 000000000..556578275
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/CodeEntryWidget.java
@@ -0,0 +1,76 @@
+package net.sf.briar.android.invitation;
+
+import static android.text.InputType.TYPE_CLASS_NUMBER;
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.content.Context;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+public class CodeEntryWidget extends LinearLayout implements
+OnEditorActionListener, OnClickListener {
+
+ private CodeEntryListener listener = null;
+ private EditText codeEntry = null;
+
+ public CodeEntryWidget(Context ctx) {
+ super(ctx);
+ }
+
+ void init(CodeEntryListener listener, String prompt) {
+ this.listener = listener;
+ setOrientation(VERTICAL);
+
+ Context ctx = getContext();
+ TextView enterCode = new TextView(ctx);
+ enterCode.setGravity(CENTER_HORIZONTAL);
+ enterCode.setText(prompt);
+ addView(enterCode);
+
+ final Button continueButton = new Button(ctx);
+ continueButton.setText(R.string.continue_button);
+ continueButton.setEnabled(false);
+ continueButton.setOnClickListener(this);
+
+ codeEntry = new EditText(ctx) {
+ @Override
+ protected void onTextChanged(CharSequence text, int start,
+ int lengthBefore, int lengthAfter) {
+ continueButton.setEnabled(text.length() == 6);
+ }
+ };
+ codeEntry.setOnEditorActionListener(this);
+ codeEntry.setMinEms(5);
+ codeEntry.setMaxEms(5);
+ codeEntry.setMaxLines(1);
+ codeEntry.setInputType(TYPE_CLASS_NUMBER);
+
+ LinearLayout innerLayout = new LinearLayout(ctx);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ innerLayout.addView(codeEntry);
+ innerLayout.addView(continueButton);
+ addView(innerLayout);
+ }
+
+ public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
+ validateAndReturnCode();
+ return true;
+ }
+
+ public void onClick(View view) {
+ validateAndReturnCode();
+ }
+
+ private void validateAndReturnCode() {
+ CharSequence code = codeEntry.getText();
+ if(code.length() == 6) listener.codeEntered(code.toString());
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/CodesDoNotMatchActivity.java b/src/net/sf/briar/android/invitation/CodesDoNotMatchActivity.java
new file mode 100644
index 000000000..c981bc067
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/CodesDoNotMatchActivity.java
@@ -0,0 +1,54 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.widget.LinearLayout.HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class CodesDoNotMatchActivity extends Activity
+implements OnClickListener {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_codes_do_not_match);
+ LinearLayout outerLayout = (LinearLayout) findViewById(
+ R.id.codes_do_not_match_container);
+
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ImageView icon = new ImageView(this);
+ icon.setImageResource(R.drawable.iconic_x_alt_red);
+ icon.setPadding(10, 10, 10, 10);
+ innerLayout.addView(icon);
+ TextView failed = new TextView(this);
+ failed.setTextSize(20);
+ failed.setText(R.string.codes_do_not_match);
+ innerLayout.addView(failed);
+ outerLayout.addView(innerLayout);
+
+ TextView interfering = new TextView(this);
+ interfering.setText(R.string.interfering);
+ outerLayout.addView(interfering);
+ Button tryAgain = new Button(this);
+ tryAgain.setText(R.string.try_again_button);
+ tryAgain.setOnClickListener(this);
+ outerLayout.addView(tryAgain);
+ }
+
+ public void onClick(View view) {
+ Intent intent = new Intent(this, InvitationCodeActivity.class);
+ intent.putExtras(getIntent().getExtras());
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/ConfirmationCodeActivity.java b/src/net/sf/briar/android/invitation/ConfirmationCodeActivity.java
new file mode 100644
index 000000000..e4bb901df
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConfirmationCodeActivity.java
@@ -0,0 +1,69 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ConfirmationCodeActivity extends Activity
+implements CodeEntryListener {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_connection_succeeded);
+ LinearLayout outerLayout = (LinearLayout) findViewById(
+ R.id.connection_succeeded_container);
+
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ImageView icon = new ImageView(this);
+ icon.setImageResource(R.drawable.iconic_check_alt_green);
+ icon.setPadding(10, 10, 10, 10);
+ innerLayout.addView(icon);
+ TextView failed = new TextView(this);
+ failed.setTextSize(20);
+ failed.setText(R.string.connected_to_contact);
+ innerLayout.addView(failed);
+ outerLayout.addView(innerLayout);
+
+ TextView checkNetwork = new TextView(this);
+ checkNetwork.setGravity(CENTER_HORIZONTAL);
+ checkNetwork.setText(R.string.your_confirmation_code);
+ outerLayout.addView(checkNetwork);
+ TextView code = new TextView(this);
+ code.setGravity(CENTER_HORIZONTAL);
+ InvitationManager im = InvitationManagerFactory.getInvitationManager();
+ String localConfirmationCode = im.getLocalConfirmationCode();
+ code.setText(localConfirmationCode);
+ code.setTextSize(50);
+ outerLayout.addView(code);
+ CodeEntryWidget codeEntry = new CodeEntryWidget(this);
+ Resources res = getResources();
+ codeEntry.init(this, res.getString(R.string.enter_confirmation_code));
+ outerLayout.addView(codeEntry);
+ }
+
+ public void codeEntered(String code) {
+ InvitationManager im = InvitationManagerFactory.getInvitationManager();
+ String remoteConfirmationCode = im.getRemoteConfirmationCode();
+ if(code.equals(String.valueOf(remoteConfirmationCode))) {
+ Intent intent = new Intent(this, WaitForContactActivity.class);
+ intent.putExtras(getIntent().getExtras());
+ startActivity(intent);
+ } else {
+ Intent intent = new Intent(this, CodesDoNotMatchActivity.class);
+ intent.putExtras(getIntent().getExtras());
+ startActivity(intent);
+ }
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/ConfirmationListener.java b/src/net/sf/briar/android/invitation/ConfirmationListener.java
new file mode 100644
index 000000000..af2024676
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConfirmationListener.java
@@ -0,0 +1,8 @@
+package net.sf.briar.android.invitation;
+
+interface ConfirmationListener {
+
+ void confirmationReceived();
+
+ void confirmationNotReceived();
+}
diff --git a/src/net/sf/briar/android/invitation/ConnectionActivity.java b/src/net/sf/briar/android/invitation/ConnectionActivity.java
new file mode 100644
index 000000000..2564adf00
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConnectionActivity.java
@@ -0,0 +1,99 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class ConnectionActivity extends Activity implements ConnectionListener {
+
+ private final InvitationManager manager =
+ InvitationManagerFactory.getInvitationManager();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_connection);
+ LinearLayout outerLayout = (LinearLayout) findViewById(
+ R.id.connection_container);
+
+ Bundle b = getIntent().getExtras();
+ String networkName = b.getString(
+ "net.sf.briar.android.invitation.NETWORK_NAME");
+ boolean useBluetooth = b.getBoolean(
+ "net.sf.briar.android.invitation.USE_BLUETOOTH");
+
+ TextView yourCode = new TextView(this);
+ yourCode.setGravity(CENTER_HORIZONTAL);
+ yourCode.setText(R.string.your_invitation_code);
+ outerLayout.addView(yourCode);
+ TextView code = new TextView(this);
+ code.setGravity(CENTER_HORIZONTAL);
+ code.setText(manager.getLocalInvitationCode());
+ code.setTextSize(50);
+ outerLayout.addView(code);
+
+ if(networkName != null) {
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ProgressBar progress = new ProgressBar(this);
+ progress.setIndeterminate(true);
+ progress.setPadding(0, 10, 10, 0);
+ innerLayout.addView(progress);
+ TextView connecting = new TextView(this);
+ Resources res = getResources();
+ String text = res.getString(R.string.connecting_wifi);
+ text = String.format(text, networkName);
+ connecting.setText(text);
+ innerLayout.addView(connecting);
+ outerLayout.addView(innerLayout);
+ manager.startWifiConnectionWorker(this);
+ }
+
+ if(useBluetooth) {
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ProgressBar progress = new ProgressBar(this);
+ progress.setPadding(0, 10, 10, 0);
+ progress.setIndeterminate(true);
+ innerLayout.addView(progress);
+ TextView connecting = new TextView(this);
+ connecting.setText(R.string.connecting_bluetooth);
+ innerLayout.addView(connecting);
+ outerLayout.addView(innerLayout);
+ manager.startBluetoothConnectionWorker(this);
+ }
+
+ manager.tryToConnect(this);
+ }
+
+ public void connectionEstablished() {
+ final Intent intent = new Intent(this, ConfirmationCodeActivity.class);
+ intent.putExtras(getIntent().getExtras());
+ runOnUiThread(new Runnable() {
+ public void run() {
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+
+ public void connectionNotEstablished() {
+ final Intent intent = new Intent(this, ConnectionFailedActivity.class);
+ runOnUiThread(new Runnable() {
+ public void run() {
+ startActivity(intent);
+ finish();
+ }
+ });
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/ConnectionFailedActivity.java b/src/net/sf/briar/android/invitation/ConnectionFailedActivity.java
new file mode 100644
index 000000000..0b868a174
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConnectionFailedActivity.java
@@ -0,0 +1,96 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static android.widget.LinearLayout.HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ConnectionFailedActivity extends Activity
+implements WifiStateListener, BluetoothStateListener, OnClickListener {
+
+ private WifiWidget wifi = null;
+ private BluetoothWidget bluetooth = null;
+ private Button tryAgainButton = null;
+ private String networkName = null;
+ private boolean useBluetooth = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_connection_failed);
+ LinearLayout outerLayout = (LinearLayout) findViewById(
+ R.id.connection_failed_container);
+
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ImageView icon = new ImageView(this);
+ icon.setImageResource(R.drawable.iconic_x_alt_red);
+ icon.setPadding(10, 10, 10, 10);
+ innerLayout.addView(icon);
+ TextView failed = new TextView(this);
+ failed.setTextSize(20);
+ failed.setText(R.string.connection_failed);
+ innerLayout.addView(failed);
+ outerLayout.addView(innerLayout);
+
+ TextView checkNetwork = new TextView(this);
+ checkNetwork.setText(R.string.check_same_network);
+ outerLayout.addView(checkNetwork);
+ wifi = new WifiWidget(this);
+ wifi.init(this);
+ outerLayout.addView(wifi);
+ bluetooth = new BluetoothWidget(this);
+ bluetooth.init(this);
+ outerLayout.addView(bluetooth);
+ tryAgainButton = new Button(this);
+ tryAgainButton.setText(R.string.try_again_button);
+ tryAgainButton.setOnClickListener(this);
+ setTryAgainButtonVisibility();
+ outerLayout.addView(tryAgainButton);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ wifi.populate();
+ bluetooth.populate();
+ }
+
+ public void wifiStateChanged(String networkName) {
+ this.networkName = networkName;
+ setTryAgainButtonVisibility();
+ }
+
+ public void bluetoothStateChanged(boolean enabled) {
+ useBluetooth = enabled;
+ setTryAgainButtonVisibility();
+ }
+
+ private void setTryAgainButtonVisibility() {
+ if(tryAgainButton == null) return;
+ if(useBluetooth || networkName != null)
+ tryAgainButton.setVisibility(VISIBLE);
+ else tryAgainButton.setVisibility(INVISIBLE);
+ }
+
+ public void onClick(View view) {
+ Intent intent = new Intent(this, InvitationCodeActivity.class);
+ intent.putExtra("net.sf.briar.android.invitation.NETWORK_NAME",
+ networkName);
+ intent.putExtra("net.sf.briar.android.invitation.USE_BLUETOOTH",
+ useBluetooth);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/ConnectionListener.java b/src/net/sf/briar/android/invitation/ConnectionListener.java
new file mode 100644
index 000000000..a1bc643dc
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ConnectionListener.java
@@ -0,0 +1,8 @@
+package net.sf.briar.android.invitation;
+
+interface ConnectionListener {
+
+ void connectionEstablished();
+
+ void connectionNotEstablished();
+}
diff --git a/src/net/sf/briar/android/invitation/ContactAddedActivity.java b/src/net/sf/briar/android/invitation/ContactAddedActivity.java
new file mode 100644
index 000000000..846c25bf1
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/ContactAddedActivity.java
@@ -0,0 +1,91 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+public class ContactAddedActivity extends Activity implements OnClickListener,
+OnEditorActionListener {
+
+ private volatile Button done = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_contact_added);
+ LinearLayout outerLayout = (LinearLayout) findViewById(
+ R.id.contact_added_container);
+
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ImageView icon = new ImageView(this);
+ icon.setImageResource(R.drawable.iconic_check_alt_green);
+ icon.setPadding(10, 10, 10, 10);
+ innerLayout.addView(icon);
+ TextView failed = new TextView(this);
+ failed.setTextSize(20);
+ failed.setText(R.string.contact_added);
+ innerLayout.addView(failed);
+ outerLayout.addView(innerLayout);
+
+ TextView enterNickname = new TextView(this);
+ enterNickname.setGravity(CENTER_HORIZONTAL);
+ enterNickname.setText(R.string.enter_nickname);
+ outerLayout.addView(enterNickname);
+ final Button addAnother = new Button(this);
+ final Button done = new Button(this);
+ this.done = done;
+ EditText nicknameEntry = new EditText(this) {
+ @Override
+ protected void onTextChanged(CharSequence text, int start,
+ int lengthBefore, int lengthAfter) {
+ addAnother.setEnabled(text.length() > 0);
+ done.setEnabled(text.length() > 0);
+ }
+ };
+ nicknameEntry.setMinEms(10);
+ nicknameEntry.setMaxEms(20);
+ nicknameEntry.setMaxLines(1);
+ nicknameEntry.setOnEditorActionListener(this);
+ outerLayout.addView(nicknameEntry);
+
+ innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ addAnother.setText(R.string.add_another_contact_button);
+ addAnother.setEnabled(false);
+ addAnother.setOnClickListener(this);
+ innerLayout.addView(addAnother);
+ done.setText(R.string.done_button);
+ done.setEnabled(false);
+ done.setOnClickListener(this);
+ innerLayout.addView(done);
+ outerLayout.addView(innerLayout);
+ }
+
+ public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
+ if(textView.getText().length() > 0) finish();
+ return true;
+ }
+
+ public void onClick(View view) {
+ if(done == null) return;
+ if(view != done)
+ startActivity(new Intent(this, NetworkSetupActivity.class));
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/InvitationCodeActivity.java b/src/net/sf/briar/android/invitation/InvitationCodeActivity.java
new file mode 100644
index 000000000..79e7a22cd
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/InvitationCodeActivity.java
@@ -0,0 +1,48 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class InvitationCodeActivity extends Activity
+implements CodeEntryListener {
+
+ private final InvitationManager manager =
+ InvitationManagerFactory.getInvitationManager();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_invitation_code);
+ LinearLayout layout = (LinearLayout) findViewById(
+ R.id.invitation_code_container);
+
+ TextView yourCode = new TextView(this);
+ yourCode.setGravity(CENTER_HORIZONTAL);
+ yourCode.setText(R.string.your_invitation_code);
+ layout.addView(yourCode);
+ TextView code = new TextView(this);
+ code.setGravity(CENTER_HORIZONTAL);
+ String localInvitationCode = manager.getLocalInvitationCode();
+ code.setText(localInvitationCode);
+ code.setTextSize(50);
+ layout.addView(code);
+ CodeEntryWidget codeEntry = new CodeEntryWidget(this);
+ Resources res = getResources();
+ codeEntry.init(this, res.getString(R.string.enter_invitation_code));
+ layout.addView(codeEntry);
+ }
+
+ public void codeEntered(String code) {
+ manager.setRemoteInvitationCode(code);
+ Intent intent = new Intent(this, ConnectionActivity.class);
+ intent.putExtras(getIntent().getExtras());
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/InvitationManager.java b/src/net/sf/briar/android/invitation/InvitationManager.java
new file mode 100644
index 000000000..f77baa4c3
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/InvitationManager.java
@@ -0,0 +1,26 @@
+package net.sf.briar.android.invitation;
+
+import android.content.Context;
+
+interface InvitationManager {
+
+ int TIMEOUT = 20 * 1000;
+
+ void tryToConnect(ConnectionListener listener);
+
+ String getLocalInvitationCode();
+
+ String getRemoteInvitationCode();
+
+ void setRemoteInvitationCode(String code);
+
+ void startWifiConnectionWorker(Context ctx);
+
+ void startBluetoothConnectionWorker(Context ctx);
+
+ String getLocalConfirmationCode();
+
+ String getRemoteConfirmationCode();
+
+ void startConfirmationWorker(ConfirmationListener listener);
+}
diff --git a/src/net/sf/briar/android/invitation/InvitationManagerFactory.java b/src/net/sf/briar/android/invitation/InvitationManagerFactory.java
new file mode 100644
index 000000000..a38ac435a
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/InvitationManagerFactory.java
@@ -0,0 +1,14 @@
+package net.sf.briar.android.invitation;
+
+class InvitationManagerFactory {
+
+ private static final Object LOCK = new Object();
+ private static InvitationManager instance = null; // Locking: lock
+
+ static InvitationManager getInvitationManager() {
+ synchronized(LOCK) {
+ if(instance == null) instance = new InvitationManagerImpl();
+ return instance;
+ }
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/InvitationManagerImpl.java b/src/net/sf/briar/android/invitation/InvitationManagerImpl.java
new file mode 100644
index 000000000..c5afcacec
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/InvitationManagerImpl.java
@@ -0,0 +1,67 @@
+package net.sf.briar.android.invitation;
+
+import android.content.Context;
+import android.util.Log;
+
+class InvitationManagerImpl implements InvitationManager {
+
+ public void tryToConnect(final ConnectionListener listener) {
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ // FIXME
+ Thread.sleep((long) (Math.random() * TIMEOUT));
+ if(Math.random() < 0.5) listener.connectionEstablished();
+ else listener.connectionNotEstablished();
+ } catch(InterruptedException e) {
+ Log.w(getClass().getName(), e.toString());
+ listener.connectionNotEstablished();
+ }
+ }
+ }.start();
+ }
+
+ public String getLocalInvitationCode() {
+ // FIXME
+ return "123456";
+ }
+
+ public String getRemoteInvitationCode() {
+ // FIXME
+ return "123456";
+ }
+
+ public void setRemoteInvitationCode(String code) {
+ // FIXME
+ }
+
+ public void startWifiConnectionWorker(Context ctx) {
+ // FIXME
+ }
+
+ public void startBluetoothConnectionWorker(Context ctx) {
+ // FIXME
+ }
+
+ public String getLocalConfirmationCode() {
+ // FIXME
+ return "123456";
+ }
+
+ public String getRemoteConfirmationCode() {
+ // FIXME
+ return "123456";
+ }
+
+ public void startConfirmationWorker(ConfirmationListener listener) {
+ // FIXME
+ try {
+ Thread.sleep(1000 + (int) (Math.random() * 4 * 1000));
+ } catch(InterruptedException e) {
+ Log.w(getClass().getName(), e.toString());
+ Thread.currentThread().interrupt();
+ }
+ listener.confirmationReceived();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/NetworkSetupActivity.java b/src/net/sf/briar/android/invitation/NetworkSetupActivity.java
new file mode 100644
index 000000000..2488e4d12
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/NetworkSetupActivity.java
@@ -0,0 +1,88 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class NetworkSetupActivity extends Activity
+implements WifiStateListener, BluetoothStateListener, OnClickListener {
+
+ private WifiWidget wifi = null;
+ private BluetoothWidget bluetooth = null;
+ private Button continueButton = null;
+ private String networkName = null;
+ private boolean useBluetooth = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_network_setup);
+ LinearLayout layout = (LinearLayout) findViewById(
+ R.id.network_setup_container);
+
+ TextView sameNetwork = new TextView(this);
+ sameNetwork.setText(R.string.same_network);
+ layout.addView(sameNetwork);
+ wifi = new WifiWidget(this);
+ wifi.init(this);
+ layout.addView(wifi);
+ bluetooth = new BluetoothWidget(this);
+ bluetooth.init(this);
+ layout.addView(bluetooth);
+ continueButton = new Button(this);
+ continueButton.setText(R.string.continue_button);
+ continueButton.setOnClickListener(this);
+ setContinueButtonVisibility();
+ layout.addView(continueButton);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ wifi.populate();
+ bluetooth.populate();
+ }
+
+ public void wifiStateChanged(final String name) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ networkName = name;
+ setContinueButtonVisibility();
+ }
+ });
+ }
+
+ public void bluetoothStateChanged(final boolean enabled) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ useBluetooth = enabled;
+ setContinueButtonVisibility();
+ }
+ });
+ }
+
+ private void setContinueButtonVisibility() {
+ if(continueButton == null) return;
+ if(useBluetooth || networkName != null)
+ continueButton.setVisibility(VISIBLE);
+ else continueButton.setVisibility(INVISIBLE);
+ }
+
+ public void onClick(View view) {
+ Intent intent = new Intent(this, InvitationCodeActivity.class);
+ intent.putExtra("net.sf.briar.android.invitation.NETWORK_NAME",
+ networkName);
+ intent.putExtra("net.sf.briar.android.invitation.USE_BLUETOOTH",
+ useBluetooth);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/WaitForContactActivity.java b/src/net/sf/briar/android/invitation/WaitForContactActivity.java
new file mode 100644
index 000000000..ed778ff23
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/WaitForContactActivity.java
@@ -0,0 +1,76 @@
+package net.sf.briar.android.invitation;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import net.sf.briar.R;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class WaitForContactActivity extends Activity
+implements ConfirmationListener {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_wait_for_contact);
+ LinearLayout outerLayout = (LinearLayout) findViewById(
+ R.id.wait_for_contact_container);
+
+ LinearLayout innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ImageView icon = new ImageView(this);
+ icon.setImageResource(R.drawable.iconic_check_alt_green);
+ icon.setPadding(10, 10, 10, 10);
+ innerLayout.addView(icon);
+ TextView failed = new TextView(this);
+ failed.setTextSize(20);
+ failed.setText(R.string.connected_to_contact);
+ innerLayout.addView(failed);
+ outerLayout.addView(innerLayout);
+
+ TextView yourCode = new TextView(this);
+ yourCode.setGravity(CENTER_HORIZONTAL);
+ yourCode.setText(R.string.your_confirmation_code);
+ outerLayout.addView(yourCode);
+ TextView code = new TextView(this);
+ code.setGravity(CENTER_HORIZONTAL);
+ InvitationManager im = InvitationManagerFactory.getInvitationManager();
+ String localConfirmationCode = im.getLocalConfirmationCode();
+ code.setText(localConfirmationCode);
+ code.setTextSize(50);
+ outerLayout.addView(code);
+
+ innerLayout = new LinearLayout(this);
+ innerLayout.setOrientation(HORIZONTAL);
+ innerLayout.setGravity(CENTER);
+ ProgressBar progress = new ProgressBar(this);
+ progress.setIndeterminate(true);
+ progress.setPadding(0, 10, 10, 0);
+ innerLayout.addView(progress);
+ TextView connecting = new TextView(this);
+ connecting.setText(R.string.waiting_for_contact);
+ innerLayout.addView(connecting);
+ outerLayout.addView(innerLayout);
+
+ im.startConfirmationWorker(this);
+ }
+
+ public void confirmationReceived() {
+ startActivity(new Intent(this, ContactAddedActivity.class));
+ finish();
+ }
+
+ public void confirmationNotReceived() {
+ Intent intent = new Intent(this, CodesDoNotMatchActivity.class);
+ intent.putExtras(getIntent().getExtras());
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/net/sf/briar/android/invitation/WifiStateListener.java b/src/net/sf/briar/android/invitation/WifiStateListener.java
new file mode 100644
index 000000000..c668b0d2d
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/WifiStateListener.java
@@ -0,0 +1,6 @@
+package net.sf.briar.android.invitation;
+
+interface WifiStateListener {
+
+ void wifiStateChanged(String networkName);
+}
diff --git a/src/net/sf/briar/android/invitation/WifiWidget.java b/src/net/sf/briar/android/invitation/WifiWidget.java
new file mode 100644
index 000000000..d27d6e8b3
--- /dev/null
+++ b/src/net/sf/briar/android/invitation/WifiWidget.java
@@ -0,0 +1,77 @@
+package net.sf.briar.android.invitation;
+
+import static android.content.Context.WIFI_SERVICE;
+import static android.provider.Settings.ACTION_WIFI_SETTINGS;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import net.sf.briar.R;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.wifi.WifiManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class WifiWidget extends LinearLayout implements OnClickListener {
+
+ private WifiStateListener listener = null;
+
+ public WifiWidget(Context ctx) {
+ super(ctx);
+ }
+
+ void init(WifiStateListener listener) {
+ this.listener = listener;
+ setOrientation(VERTICAL);
+ setPadding(0, 10, 0, 0);
+ populate();
+ }
+
+ void populate() {
+ removeAllViews();
+ Context ctx = getContext();
+ TextView status = new TextView(ctx);
+ status.setGravity(CENTER_HORIZONTAL);
+ WifiManager wifi = (WifiManager) ctx.getSystemService(WIFI_SERVICE);
+ if(wifi == null) {
+ wifiStateChanged(null);
+ status.setText(R.string.wifi_not_available);
+ addView(status);
+ } else if(wifi.isWifiEnabled()) {
+ String networkName = wifi.getConnectionInfo().getSSID();
+ if(networkName == null) {
+ wifiStateChanged(null);
+ status.setText(R.string.wifi_disconnected);
+ addView(status);
+ Button connect = new Button(ctx);
+ connect.setText(R.string.connect_to_wifi_button);
+ connect.setOnClickListener(this);
+ addView(connect);
+ } else {
+ wifiStateChanged(networkName);
+ Resources res = getResources();
+ String connected = res.getString(R.string.wifi_connected);
+ status.setText(String.format(connected, networkName));
+ addView(status);
+ }
+ } else {
+ wifiStateChanged(null);
+ status.setText(R.string.wifi_disabled);
+ addView(status);
+ Button connect = new Button(ctx);
+ connect.setText(R.string.connect_to_wifi_button);
+ connect.setOnClickListener(this);
+ addView(connect);
+ }
+ }
+
+ private void wifiStateChanged(String networkName) {
+ if(listener != null) listener.wifiStateChanged(networkName);
+ }
+
+ public void onClick(View view) {
+ getContext().startActivity(new Intent(ACTION_WIFI_SETTINGS));
+ }
+}