Skip to content
Snippets Groups Projects
AddContactActivity.java 13.1 KiB
Newer Older
package org.briarproject.android.invitation;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;

import java.util.Collection;
import java.util.logging.Logger;

import javax.inject.Inject;

import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.api.AuthorId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.TransportConfig;
import org.briarproject.api.TransportId;
import org.briarproject.api.android.ReferenceManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.invitation.InvitationListener;
import org.briarproject.api.invitation.InvitationState;
import org.briarproject.api.invitation.InvitationTask;
import org.briarproject.api.invitation.InvitationTaskFactory;
akwizgran's avatar
akwizgran committed

import android.bluetooth.BluetoothAdapter;
import android.content.Intent;
import android.widget.Toast;
public class AddContactActivity extends BriarActivity
implements InvitationListener {
	static final int REQUEST_BLUETOOTH = 1;
	static final int REQUEST_CREATE_IDENTITY = 2;

	private static final Logger LOG =
			Logger.getLogger(AddContactActivity.class.getName());

	@Inject private CryptoComponent crypto;
	@Inject private InvitationTaskFactory invitationTaskFactory;
	@Inject private ReferenceManager referenceManager;
	private AddContactView view = null;
	private InvitationTask task = null;
	private AuthorId localAuthorId = null;
	private int localInvitationCode = -1, remoteInvitationCode = -1;
	private int localConfirmationCode = -1, remoteConfirmationCode = -1;
	private boolean connected = false, connectionFailed = false;
	private boolean localCompared = false, remoteCompared = false;
	private boolean localMatched = false, remoteMatched = false;
	private String contactName = null;
	private boolean bluetoothWasEnabled = false;
	// Fields that are accessed from background threads must be volatile
	@Inject private volatile DatabaseComponent db;
	private volatile boolean enableBluetooth = true;
	@Override
	public void onCreate(Bundle state) {
akwizgran's avatar
akwizgran committed
		super.onCreate(state);
		if(state == null) {
			// This is a new activity
			setView(new ChooseIdentityView(this));
		} else {
			// Restore the activity's state
			byte[] b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
			if(b != null) localAuthorId = new AuthorId(b);
			taskHandle = state.getLong("briar.TASK_HANDLE", -1);
			task = referenceManager.getReference(taskHandle,
					InvitationTask.class);
			if(task == null) {
				// No background task - we must be in an initial or final state
				localInvitationCode = state.getInt("briar.LOCAL_CODE");
				remoteInvitationCode = state.getInt("briar.REMOTE_CODE");
				connectionFailed = state.getBoolean("briar.FAILED");
				contactName = state.getString("briar.CONTACT_NAME");
					localCompared = remoteCompared = true;
					localMatched = remoteMatched = true;
				}
				// Set the appropriate view for the state
				if(localInvitationCode == -1) {
					setView(new ChooseIdentityView(this));
				} else if(remoteInvitationCode == -1) {
					setView(new InvitationCodeView(this));
				} else if(connectionFailed) {
					setView(new ConnectionFailedView(this));
				} else if(contactName == null) {
					setView(new CodesDoNotMatchView(this));
					showToastAndFinish();
					return;
				}
			} else {
				// A background task exists - listen to it and get its state
				InvitationState s = task.addListener(this);
				localInvitationCode = s.getLocalInvitationCode();
				remoteInvitationCode = s.getRemoteInvitationCode();
				localConfirmationCode = s.getLocalConfirmationCode();
				remoteConfirmationCode = s.getRemoteConfirmationCode();
				connected = s.getConnected();
				connectionFailed = s.getConnectionFailed();
				localCompared = s.getLocalCompared();
				remoteCompared = s.getRemoteCompared();
				localMatched = s.getLocalMatched();
				remoteMatched = s.getRemoteMatched();
				contactName = s.getContactName();
				// Set the appropriate view for the state
				if(localInvitationCode == -1) {
					setView(new ChooseIdentityView(this));
				} else if(remoteInvitationCode == -1) {
					setView(new InvitationCodeView(this));
				} else if(connectionFailed) {
					setView(new ConnectionFailedView(this));
				} else if(connected && localConfirmationCode == -1) {
					setView(new ConnectedView(this));
				} else if(localConfirmationCode == -1) {
					setView(new ConnectionView(this));
				} else if(!localCompared) {
					setView(new ConfirmationCodeView(this));
				} else if(!remoteCompared) {
					setView(new WaitForContactView(this));
				} else if(localMatched && remoteMatched) {
					if(contactName == null) {
						setView(new ContactDetailsView(this));
					} else {
						showToastAndFinish();
						return;
					}
				} else {
					setView(new CodesDoNotMatchView(this));
				}
			}
		}
		BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
		if(adapter != null) bluetoothWasEnabled = adapter.isEnabled();
	private void showToastAndFinish() {
		String format = getString(R.string.contact_added_toast);
		String text = String.format(format, contactName);
		Toast.makeText(this, text, LENGTH_LONG).show();
	@Override
	public void onResume() {
		super.onResume();
		loadBluetoothSetting();
	}

	private void loadBluetoothSetting() {
		runOnDbThread(new Runnable() {
			public void run() {
				try {
					long now = System.currentTimeMillis();
					TransportConfig c = db.getConfig(new TransportId("bt"));
					long duration = System.currentTimeMillis() - now;
					if(LOG.isLoggable(INFO))
						LOG.info("Loading setting took " + duration + " ms");
					enableBluetooth = c.getBoolean("enable", true);
				} catch(DbException e) {
					if(LOG.isLoggable(WARNING))
						LOG.log(WARNING, e.toString(), e);
				}
			}
		});
	}

	@Override
	public void onSaveInstanceState(Bundle state) {
akwizgran's avatar
akwizgran committed
		super.onSaveInstanceState(state);
			byte[] b = localAuthorId.getBytes();
			state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
		state.putInt("briar.LOCAL_CODE", localInvitationCode);
		state.putInt("briar.REMOTE_CODE", remoteInvitationCode);
		state.putBoolean("briar.FAILED", connectionFailed);
		state.putString("briar.CONTACT_NAME", contactName);
		if(task != null) state.putLong("briar.TASK_HANDLE", taskHandle);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		if(task != null) task.removeListener(this);
		if(!bluetoothWasEnabled && !enableBluetooth) {
			BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
			if(adapter != null) adapter.disable();
		}
	}

	@Override
	public void onActivityResult(int request, int result, Intent data) {
		if(request == REQUEST_BLUETOOTH) {
			if(result != RESULT_CANCELED) reset(new InvitationCodeView(this));
		} else if(request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
			byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
			if(b == null) throw new IllegalStateException();
			localAuthorId = new AuthorId(b);
			setView(new ChooseIdentityView(this));
		}
	}

	void setView(AddContactView view) {
		this.view = view;
		view.init(this);
		setContentView(view);
	}

	void reset(AddContactView view) {
		// Don't reset localAuthorId
		localInvitationCode = -1;
		localConfirmationCode = remoteConfirmationCode = -1;
		connected = connectionFailed = false;
		localCompared = remoteCompared = false;
		localMatched = remoteMatched = false;
		runOnDbThread(new Runnable() {
			public void run() {
				try {
					long now = System.currentTimeMillis();
					Collection<LocalAuthor> authors = db.getLocalAuthors();
					long duration = System.currentTimeMillis() - now;
					if(LOG.isLoggable(INFO))
						LOG.info("Loading authors took " + duration + " ms");
					displayLocalAuthors(authors);
				} catch(DbException e) {
					if(LOG.isLoggable(WARNING))
						LOG.log(WARNING, e.toString(), e);
				}
			}
		});
	}

	// FIXME: The interaction between views and the container is horrible
	private void displayLocalAuthors(final Collection<LocalAuthor> authors) {
		runOnUiThread(new Runnable() {
			public void run() {
				AddContactView view = AddContactActivity.this.view;
				if(view instanceof ChooseIdentityView)
					((ChooseIdentityView) view).displayLocalAuthors(authors);
	void setLocalAuthorId(AuthorId localAuthorId) {
		this.localAuthorId = localAuthorId;
	}

	AuthorId getLocalAuthorId() {
		return localAuthorId;
	}

	int getLocalInvitationCode() {
		if(localInvitationCode == -1)
			localInvitationCode = crypto.generateInvitationCode();
	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, enableBluetooth);
		taskHandle = referenceManager.putReference(task, InvitationTask.class);
		task.addListener(AddContactActivity.this);
		// Add a second listener so we can remove the first in onDestroy(),
		// allowing the activity to be garbage collected if it's destroyed
		task.addListener(new ReferenceCleaner(referenceManager, taskHandle));
	}

	int getLocalConfirmationCode() {
		return localConfirmationCode;
	}

	void remoteConfirmationCodeEntered(int code) {
		if(code == remoteConfirmationCode) {
			localMatched = true;
			if(remoteMatched) setView(new ContactDetailsView(this));
			else if(remoteCompared) setView(new CodesDoNotMatchView(this));
			else setView(new WaitForContactView(this));
			task.localConfirmationSucceeded();
			setView(new CodesDoNotMatchView(this));
			task.localConfirmationFailed();
	String getContactName() {
		return contactName;
	public void connectionSucceeded() {
		runOnUiThread(new Runnable() {
			public void run() {
				connected = true;
				setView(new ConnectedView(AddContactActivity.this));
			}
		});
	}

	public void connectionFailed() {
		runOnUiThread(new Runnable() {
			public void run() {
				connectionFailed = true;
				setView(new ConnectionFailedView(AddContactActivity.this));
			}
		});
	}

	public void keyAgreementSucceeded(final int localCode,
			final int remoteCode) {
		runOnUiThread(new Runnable() {
			public void run() {
				localConfirmationCode = localCode;
				remoteConfirmationCode = remoteCode;
				setView(new ConfirmationCodeView(AddContactActivity.this));
			}
		});
	}

	public void keyAgreementFailed() {
		runOnUiThread(new Runnable() {
			public void run() {
				setView(new ConnectionFailedView(AddContactActivity.this));
			}
		});
	}

	public void remoteConfirmationSucceeded() {
		runOnUiThread(new Runnable() {
			public void run() {
				remoteCompared = true;
				remoteMatched = true;
				if(localMatched)
					setView(new ContactDetailsView(AddContactActivity.this));
	public void remoteConfirmationFailed() {
		runOnUiThread(new Runnable() {
			public void run() {
				remoteCompared = true;
				if(localMatched)
					setView(new CodesDoNotMatchView(AddContactActivity.this));
			}
		});
	}
	public void pseudonymExchangeSucceeded(final String remoteName) {
		runOnUiThread(new Runnable() {
			public void run() {
				contactName = remoteName;
			}
		});
	}

	public void pseudonymExchangeFailed() {
		runOnUiThread(new Runnable() {
			public void run() {
				setView(new ConnectionFailedView(AddContactActivity.this));
			}
		});
	}

	/**
	 * Cleans up the reference to the invitation task when the task completes.
	 * This class is static to prevent memory leaks.
	 */
	private static class ReferenceCleaner implements InvitationListener {

		private final ReferenceManager referenceManager;
		private final long handle;

		private ReferenceCleaner(ReferenceManager referenceManager,
				long handle) {
			this.referenceManager = referenceManager;
			this.handle = handle;
		}

		public void connectionSucceeded() {
			// Wait for key agreement to succeed or fail
		}

		public void connectionFailed() {
			referenceManager.removeReference(handle, InvitationTask.class);
		}

		public void keyAgreementSucceeded(int localCode, int remoteCode) {
			// Wait for remote confirmation to succeed or fail
		}

		public void keyAgreementFailed() {
			referenceManager.removeReference(handle, InvitationTask.class);
		}

		public void remoteConfirmationSucceeded() {
			// Wait for the pseudonym exchange to succeed or fail
		}

		public void remoteConfirmationFailed() {
			referenceManager.removeReference(handle, InvitationTask.class);
		}

		public void pseudonymExchangeSucceeded(String remoteName) {
			referenceManager.removeReference(handle, InvitationTask.class);
		}

		public void pseudonymExchangeFailed() {
			referenceManager.removeReference(handle, InvitationTask.class);
		}