Merge branch '117-qr-contacts' into 'master'

BQP with QR codes

This MR implements BQP for key agreement over short-range transports. It also implements the Android UI for using BQP with QR codes.

Closes #117.

See merge request !84
This commit is contained in:
akwizgran
2016-03-31 11:21:02 +00:00
63 changed files with 3571 additions and 125 deletions

View File

@@ -13,6 +13,9 @@ import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.WriteForumPostActivity;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.keyagreement.ChooseIdentityFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.keyagreement.ShowQrCodeFragment;
import org.briarproject.android.panic.PanicPreferencesActivity;
import org.briarproject.android.panic.PanicResponderActivity;
import org.briarproject.plugins.AndroidPluginsModule;
@@ -44,6 +47,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
void inject(AddContactActivity activity);
void inject(KeyAgreementActivity activity);
void inject(ConversationActivity activity);
void inject(CreateIdentityActivity activity);
@@ -68,6 +73,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
void inject(ForumListFragment fragment);
void inject(ChooseIdentityFragment fragment);
void inject(ShowQrCodeFragment fragment);
// Eager singleton load
void inject(AndroidModule.EagerSingletons init);

View File

@@ -42,7 +42,8 @@ public abstract class BriarFragmentActivity extends BriarActivity {
@Override
public void onBackPressed() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0 &&
if (this instanceof NavDrawerActivity &&
getSupportFragmentManager().getBackStackEntryCount() == 0 &&
getSupportFragmentManager()
.findFragmentByTag(ContactListFragment.TAG) == null) {
/*

View File

@@ -13,7 +13,7 @@ import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarApplication;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
@@ -107,7 +107,7 @@ public class ContactListFragment extends BaseEventFragment {
@Override
public void onClick(View v) {
startActivity(new Intent(getContext(),
AddContactActivity.class));
KeyAgreementActivity.class));
}
});

View File

@@ -0,0 +1,193 @@
package org.briarproject.android.keyagreement;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Spinner;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorItemComparator;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.app.Activity.RESULT_OK;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.identity.LocalAuthorItem.NEW;
public class ChooseIdentityFragment extends BaseFragment
implements OnItemSelectedListener {
interface IdentitySelectedListener {
void identitySelected(AuthorId localAuthorId);
}
private static final Logger LOG =
Logger.getLogger(ChooseIdentityFragment.class.getName());
public static final String TAG = "ChooseIdentityFragment";
private static final int REQUEST_CREATE_IDENTITY = 1;
private IdentitySelectedListener lsnr;
private LocalAuthorSpinnerAdapter adapter;
private Spinner spinner;
private View button;
private AuthorId localAuthorId;
// Fields that are accessed from background threads must be volatile
@Inject
protected volatile IdentityManager identityManager;
public static ChooseIdentityFragment newInstance() {
Bundle args = new Bundle();
ChooseIdentityFragment fragment = new ChooseIdentityFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
lsnr = (IdentitySelectedListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(
"Using class must implement IdentitySelectedListener");
}
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.invitation_bluetooth_start, container,
false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
adapter = new LocalAuthorSpinnerAdapter(getActivity(), false);
spinner = (Spinner) view.findViewById(R.id.spinner);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this);
button = view.findViewById(R.id.continueButton);
button.setEnabled(false);
button.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
lsnr.identitySelected(localAuthorId);
}
});
loadLocalAuthors();
}
private void loadLocalAuthors() {
listener.runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
Collection<LocalAuthor> authors =
identityManager.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);
}
}
});
}
private void displayLocalAuthors(final Collection<LocalAuthor> authors) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.clear();
for (LocalAuthor a : authors)
adapter.add(new LocalAuthorItem(a));
adapter.sort(LocalAuthorItemComparator.INSTANCE);
// If a local author has been selected, select it again
if (localAuthorId == null) return;
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
LocalAuthorItem item = adapter.getItem(i);
if (item == NEW) continue;
if (item.getLocalAuthor().getId().equals(localAuthorId)) {
spinner.setSelection(i);
return;
}
}
}
});
}
private void setLocalAuthorId(AuthorId authorId) {
localAuthorId = authorId;
button.setEnabled(localAuthorId != null);
}
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
LocalAuthorItem item = adapter.getItem(position);
if (item == NEW) {
setLocalAuthorId(null);
Intent i = new Intent(getActivity(), CreateIdentityActivity.class);
startActivityForResult(i, REQUEST_CREATE_IDENTITY);
} else {
setLocalAuthorId(item.getLocalAuthor().getId());
}
}
public void onNothingSelected(AdapterView<?> parent) {
setLocalAuthorId(null);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
if (b == null) throw new IllegalStateException();
setLocalAuthorId(new AuthorId(b));
loadLocalAuthors();
} else
super.onActivityResult(request, result, data);
}
}

View File

@@ -0,0 +1,230 @@
package org.briarproject.android.keyagreement;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarFragmentActivity;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.CustomAnimations;
import org.briarproject.api.contact.ContactExchangeListener;
import org.briarproject.api.contact.ContactExchangeTask;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.KeyAgreementFinishedEvent;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.keyagreement.KeyAgreementResult;
import org.briarproject.api.settings.SettingsManager;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
public class KeyAgreementActivity extends BriarFragmentActivity implements
BaseFragment.BaseFragmentListener,
ChooseIdentityFragment.IdentitySelectedListener, EventListener,
ContactExchangeListener {
private static final Logger LOG =
Logger.getLogger(KeyAgreementActivity.class.getName());
private static final String LOCAL_AUTHOR_ID = "briar.LOCAL_AUTHOR_ID";
private static final int STEP_ID = 1;
private static final int STEP_QR = 2;
private static final int STEPS = 2;
@Inject
protected EventBus eventBus;
@Inject
protected SettingsManager settingsManager;
private Toolbar toolbar;
private View progressContainer;
private TextView progressTitle;
private AuthorId localAuthorId;
@Inject
protected volatile ContactExchangeTask contactExchangeTask;
@Inject
protected volatile IdentityManager identityManager;
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@SuppressWarnings("ConstantConditions")
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_with_loading);
toolbar = (Toolbar) findViewById(R.id.toolbar);
progressContainer = findViewById(R.id.container_progress);
progressTitle = (TextView) findViewById(R.id.title_progress_bar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (state != null) {
byte[] b = state.getByteArray(LOCAL_AUTHOR_ID);
if (b != null)
localAuthorId = new AuthorId(b);
}
showStep(localAuthorId == null ? STEP_ID : STEP_QR);
}
@SuppressWarnings("ConstantConditions")
private void showStep(int step) {
getSupportActionBar().setTitle(
String.format(getString(R.string.add_contact_title_step), step,
STEPS));
switch (step) {
case STEP_QR:
startFragment(ShowQrCodeFragment.newInstance());
break;
case STEP_ID:
default:
startFragment(ChooseIdentityFragment.newInstance());
break;
}
}
@Override
public void onResume() {
super.onResume();
eventBus.addListener(this);
}
@Override
protected void onPause() {
super.onPause();
eventBus.removeListener(this);
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
if (localAuthorId != null) {
byte[] b = localAuthorId.getBytes();
state.putByteArray(LOCAL_AUTHOR_ID, b);
}
}
@Override
public void showLoadingScreen(boolean isBlocking, int stringId) {
if (isBlocking) {
CustomAnimations.animateHeight(toolbar, false, 250);
}
progressTitle.setText(stringId);
progressContainer.setVisibility(View.VISIBLE);
}
@Override
public void hideLoadingScreen() {
CustomAnimations.animateHeight(toolbar, true, 250);
progressContainer.setVisibility(View.INVISIBLE);
}
@Override
public void identitySelected(AuthorId localAuthorId) {
this.localAuthorId = localAuthorId;
showStep(STEP_QR);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof KeyAgreementFinishedEvent) {
KeyAgreementFinishedEvent event = (KeyAgreementFinishedEvent) e;
keyAgreementFinished(event.getResult());
}
}
private void keyAgreementFinished(final KeyAgreementResult result) {
runOnUiThread(new Runnable() {
@Override
public void run() {
showLoadingScreen(false, R.string.exchanging_contact_details);
startContactExchange(result);
}
});
}
private void startContactExchange(final KeyAgreementResult result) {
runOnDbThread(new Runnable() {
@Override
public void run() {
LocalAuthor localAuthor;
// Load the local pseudonym
try {
localAuthor = identityManager.getLocalAuthor(localAuthorId);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
contactExchangeFailed();
return;
}
// Exchange contact details
contactExchangeTask.startExchange(KeyAgreementActivity.this,
localAuthor, result.getMasterKey(),
result.getConnection(), result.getTransportId(),
result.wasAlice());
}
});
}
@Override
public void contactExchangeSucceeded(final Author remoteAuthor) {
runOnUiThread(new Runnable() {
public void run() {
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_added_toast);
String text = String.format(format, contactName);
Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG)
.show();
finish();
}
});
}
@Override
public void duplicateContact(final Author remoteAuthor) {
runOnUiThread(new Runnable() {
public void run() {
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_already_exists);
String text = String.format(format, contactName);
Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG)
.show();
finish();
}
});
}
@Override
public void contactExchangeFailed() {
runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(KeyAgreementActivity.this,
R.string.contact_exchange_failed, LENGTH_LONG).show();
finish();
}
});
}
}

View File

@@ -0,0 +1,371 @@
package org.briarproject.android.keyagreement;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Camera;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Base64;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.Result;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.util.AndroidUtils;
import org.briarproject.android.util.CameraView;
import org.briarproject.android.util.QrCodeDecoder;
import org.briarproject.android.util.QrCodeUtils;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.KeyAgreementAbortedEvent;
import org.briarproject.api.event.KeyAgreementFailedEvent;
import org.briarproject.api.event.KeyAgreementListeningEvent;
import org.briarproject.api.event.KeyAgreementStartedEvent;
import org.briarproject.api.event.KeyAgreementWaitingEvent;
import org.briarproject.api.keyagreement.KeyAgreementTask;
import org.briarproject.api.keyagreement.KeyAgreementTaskFactory;
import org.briarproject.api.keyagreement.Payload;
import org.briarproject.api.keyagreement.PayloadEncoder;
import org.briarproject.api.keyagreement.PayloadParser;
import java.io.IOException;
import java.util.logging.Logger;
import javax.inject.Inject;
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.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
public class ShowQrCodeFragment extends BaseEventFragment
implements QrCodeDecoder.ResultCallback {
private static final Logger LOG =
Logger.getLogger(ShowQrCodeFragment.class.getName());
public static final String TAG = "ShowQrCodeFragment";
@Inject
protected KeyAgreementTaskFactory keyAgreementTaskFactory;
@Inject
protected PayloadEncoder payloadEncoder;
@Inject
protected PayloadParser payloadParser;
private LinearLayout qrLayout;
private CameraView cameraView;
private TextView status;
private ImageView qrCode;
private BluetoothStateReceiver receiver;
private QrCodeDecoder decoder;
private boolean gotRemotePayload;
private volatile KeyAgreementTask task;
private volatile BluetoothAdapter adapter;
private volatile boolean waitingForBluetooth;
public static ShowQrCodeFragment newInstance() {
Bundle args = new Bundle();
ShowQrCodeFragment fragment = new ShowQrCodeFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
qrLayout = (LinearLayout) view.findViewById(R.id.qr_layout);
cameraView = (CameraView) view.findViewById(R.id.camera_view);
status = (TextView) view.findViewById(R.id.connect_status);
qrCode = (ImageView) view.findViewById(R.id.qr_code);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
decoder = new QrCodeDecoder(this);
Display display = getActivity().getWindowManager().getDefaultDisplay();
boolean portrait = display.getWidth() < display.getHeight();
qrLayout.setOrientation(portrait ? VERTICAL : HORIZONTAL);
adapter = BluetoothAdapter.getDefaultAdapter();
}
@Override
public void onStart() {
super.onStart();
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
receiver = new BluetoothStateReceiver();
getActivity().registerReceiver(receiver, filter);
// Enable BT adapter if it is not already on.
if (adapter != null && !adapter.isEnabled()) {
waitingForBluetooth = true;
AndroidUtils.enableBluetooth(adapter, true);
} else
startListening();
}
@Override
public void onResume() {
super.onResume();
if (!gotRemotePayload) openCamera();
}
@Override
public void onPause() {
super.onPause();
if (!gotRemotePayload) releaseCamera();
}
@Override
public void onStop() {
super.onStop();
stopListening();
if (receiver != null) getActivity().unregisterReceiver(receiver);
}
private void startListening() {
task = keyAgreementTaskFactory.getTask();
gotRemotePayload = false;
new Thread(new Runnable() {
@Override
public void run() {
task.listen();
}
}).start();
}
private void stopListening() {
new Thread(new Runnable() {
@Override
public void run() {
task.stopListening();
}
}).start();
}
private void openCamera() {
AsyncTask<Void, Void, Camera> openTask =
new AsyncTask<Void, Void, Camera>() {
@Override
protected Camera doInBackground(Void... unused) {
LOG.info("Opening camera");
try {
return Camera.open();
} catch (RuntimeException e) {
LOG.log(WARNING,
"Error opening camera, trying again", e);
try {
Thread.sleep(1000);
} catch (InterruptedException e2) {
LOG.info("Interrupted before second attempt");
return null;
}
try {
return Camera.open();
} catch (RuntimeException e2) {
LOG.log(WARNING, "Error opening camera", e2);
return null;
}
}
}
@Override
protected void onPostExecute(Camera camera) {
if (camera == null) {
// TODO better solution?
getActivity().finish();
} else {
cameraView.start(camera, decoder, 0);
}
}
};
openTask.execute();
}
private void releaseCamera() {
LOG.info("Releasing camera");
try {
cameraView.stop();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error releasing camera", e);
// TODO better solution
getActivity().finish();
}
}
private void reset() {
cameraView.setVisibility(View.VISIBLE);
startListening();
openCamera();
}
private void qrCodeScanned(String content) {
try {
// TODO use Base32
Payload remotePayload = payloadParser.parse(
Base64.decode(content, 0));
cameraView.setVisibility(View.GONE);
status.setText(R.string.connecting_to_device);
task.connectAndRunProtocol(remotePayload);
} catch (IOException e) {
// TODO show failure
Toast.makeText(getActivity(), R.string.qr_code_invalid,
LENGTH_LONG).show();
}
}
@Override
public void eventOccurred(Event e) {
if (e instanceof KeyAgreementListeningEvent) {
KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
setQrCode(event.getLocalPayload());
} else if (e instanceof KeyAgreementFailedEvent) {
keyAgreementFailed();
} else if (e instanceof KeyAgreementWaitingEvent) {
keyAgreementWaiting();
} else if (e instanceof KeyAgreementStartedEvent) {
keyAgreementStarted();
} else if (e instanceof KeyAgreementAbortedEvent) {
KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e;
keyAgreementAborted(event.didRemoteAbort());
}
}
private void setQrCode(final Payload localPayload) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO use Base32
String input = Base64.encodeToString(
payloadEncoder.encode(localPayload), 0);
qrCode.setImageBitmap(
QrCodeUtils.createQrCode(getActivity(), input));
// Simple fade-in animation
AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
anim.setDuration(200);
qrCode.startAnimation(anim);
}
});
}
private void keyAgreementFailed() {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
reset();
// TODO show failure somewhere persistent?
Toast.makeText(getActivity(), R.string.connection_failed,
LENGTH_LONG).show();
}
});
}
private void keyAgreementWaiting() {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
status.setText(R.string.waiting_for_contact);
}
});
}
private void keyAgreementStarted() {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.showLoadingScreen(false,
R.string.authenticating_with_device);
}
});
}
private void keyAgreementAborted(final boolean remoteAborted) {
listener.runOnUiThread(new Runnable() {
@Override
public void run() {
reset();
listener.hideLoadingScreen();
// TODO show abort somewhere persistent?
Toast.makeText(getActivity(),
remoteAborted ? R.string.connection_aborted_remote :
R.string.connection_aborted_local, LENGTH_LONG)
.show();
}
});
}
@Override
public void handleResult(final Result result) {
listener.runOnUiThread(new Runnable() {
public void run() {
LOG.info("Got result from decoder");
if (!gotRemotePayload) {
gotRemotePayload = true;
releaseCamera();
qrCodeScanned(result.getText());
}
}
});
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
if (state == STATE_ON && waitingForBluetooth) {
LOG.info("Bluetooth enabled");
waitingForBluetooth = false;
startListening();
}
}
}
}

View File

@@ -0,0 +1,268 @@
package org.briarproject.android.util;
import android.content.Context;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.Build;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO;
import static android.hardware.Camera.Parameters.FOCUS_MODE_EDOF;
import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
import static android.view.SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
AutoFocusCallback {
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
private static final Logger LOG =
Logger.getLogger(CameraView.class.getName());
private Camera camera = null;
private PreviewConsumer previewConsumer = null;
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
private boolean autoFocus = false, surfaceExists = false;
public CameraView(Context context) {
super(context);
initialize();
}
public CameraView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
setKeepScreenOn(true);
SurfaceHolder holder = getHolder();
if (Build.VERSION.SDK_INT < 11)
holder.setType(SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(this);
}
public void start(Camera camera, PreviewConsumer previewConsumer,
int rotationDegrees) {
this.camera = camera;
this.previewConsumer = previewConsumer;
setDisplayOrientation(rotationDegrees);
Parameters params = camera.getParameters();
setFocusMode(params);
setPreviewSize(params);
applyParameters(params);
if (surfaceExists) startPreview(getHolder());
}
public void stop() {
stopPreview();
try {
camera.release();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error releasing camera", e);
}
camera = null;
}
private void startPreview(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
if (autoFocus) camera.autoFocus(this);
previewConsumer.start(camera);
} catch (IOException e) {
LOG.log(WARNING, "Error starting camera preview", e);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error starting camera preview", e);
}
}
private void stopPreview() {
try {
previewConsumer.stop();
if (autoFocus) camera.cancelAutoFocus();
camera.stopPreview();
} catch (RuntimeException e) {
LOG.log(WARNING, "Error stopping camera preview", e);
}
}
private void setDisplayOrientation(int rotationDegrees) {
int orientation;
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(0, info);
if (info.facing == CAMERA_FACING_FRONT) {
orientation = (info.orientation + rotationDegrees) % 360;
orientation = (360 - orientation) % 360;
} else {
orientation = (info.orientation - rotationDegrees + 360) % 360;
}
if(LOG.isLoggable(INFO))
LOG.info("Display orientation " + orientation + " degrees");
try {
camera.setDisplayOrientation(orientation);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error setting display orientation", e);
}
displayOrientation = orientation;
}
private void setFocusMode(Parameters params) {
if (Build.VERSION.SDK_INT >= 15 &&
params.isVideoStabilizationSupported()) {
LOG.info("Enabling video stabilisation");
params.setVideoStabilization(true);
}
// This returns null on the HTC Wildfire S
List<String> sceneModes = params.getSupportedSceneModes();
if (sceneModes == null) sceneModes = Collections.emptyList();
List<String> focusModes = params.getSupportedFocusModes();
if (LOG.isLoggable(INFO)) {
LOG.info("Scene modes: " + sceneModes);
LOG.info("Focus modes: " + focusModes);
}
if (sceneModes.contains(SCENE_MODE_BARCODE)) {
LOG.info("Setting scene mode to barcode");
params.setSceneMode(SCENE_MODE_BARCODE);
} else if (Build.VERSION.SDK_INT >= 14 &&
focusModes.contains(FOCUS_MODE_CONTINUOUS_PICTURE)) {
LOG.info("Setting focus mode to continuous picture");
params.setFocusMode(FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (focusModes.contains(FOCUS_MODE_CONTINUOUS_VIDEO)) {
LOG.info("Setting focus mode to continuous video");
params.setFocusMode(FOCUS_MODE_CONTINUOUS_VIDEO);
} else if (focusModes.contains(FOCUS_MODE_EDOF)) {
LOG.info("Setting focus mode to EDOF");
params.setFocusMode(FOCUS_MODE_EDOF);
} else if (focusModes.contains(FOCUS_MODE_MACRO)) {
LOG.info("Setting focus mode to macro");
params.setFocusMode(FOCUS_MODE_MACRO);
autoFocus = true;
} else if (focusModes.contains(FOCUS_MODE_AUTO)) {
LOG.info("Setting focus mode to auto");
params.setFocusMode(FOCUS_MODE_AUTO);
autoFocus = true;
} else if (focusModes.contains(FOCUS_MODE_FIXED)) {
LOG.info("Setting focus mode to fixed");
params.setFocusMode(FOCUS_MODE_FIXED);
} else {
LOG.info("No suitable focus mode");
}
params.setZoom(0);
}
private void setPreviewSize(Parameters params) {
if (surfaceWidth == 0 || surfaceHeight == 0) return;
float idealRatio = (float) surfaceWidth / surfaceHeight;
DisplayMetrics screen = getContext().getResources().getDisplayMetrics();
int screenMax = Math.max(screen.widthPixels, screen.heightPixels);
boolean rotatePreview = displayOrientation % 180 == 90;
List<Size> sizes = params.getSupportedPreviewSizes();
Size bestSize = null;
float bestScore = 0;
for (Size size : sizes) {
int width = rotatePreview ? size.height : size.width;
int height = rotatePreview ? size.width : size.height;
float ratio = (float) width / height;
float stretch = Math.max(ratio / idealRatio, idealRatio / ratio);
int pixels = width * height;
float score = width * height / stretch;
if (LOG.isLoggable(INFO)) {
LOG.info("Size " + size.width + "x" + size.height
+ ", stretch " + stretch + ", pixels " + pixels
+ ", score " + score);
}
// Large preview sizes can crash older devices
int maxDimension = Math.max(width, height);
if (Build.VERSION.SDK_INT < 14 && maxDimension > screenMax) {
LOG.info("Too large for screen");
continue;
}
if (bestSize == null || score > bestScore) {
bestSize = size;
bestScore = score;
}
}
if (bestSize != null) {
if(LOG.isLoggable(INFO))
LOG.info("Best size " + bestSize.width + "x" + bestSize.height);
params.setPreviewSize(bestSize.width, bestSize.height);
}
}
private void applyParameters(Parameters params) {
try {
camera.setParameters(params);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error setting camera parameters", e);
}
}
public void surfaceCreated(SurfaceHolder holder) {
LOG.info("Surface created");
surfaceExists = true;
if (camera != null) startPreview(holder);
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
if (LOG.isLoggable(INFO)) LOG.info("Surface changed: " + w + "x" + h);
surfaceWidth = w;
surfaceHeight = h;
if (camera == null) return; // We are stopped
stopPreview();
try {
Parameters params = camera.getParameters();
setPreviewSize(params);
applyParameters(params);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error getting camera parameters", e);
}
startPreview(holder);
}
public void surfaceDestroyed(SurfaceHolder holder) {
LOG.info("Surface destroyed");
surfaceExists = false;
holder.removeCallback(this);
}
public void onAutoFocus(boolean success, final Camera camera) {
LOG.info("Auto focus succeeded: " + success);
postDelayed(new Runnable() {
public void run() {
retryAutoFocus();
}
}, AUTO_FOCUS_RETRY_DELAY);
}
private void retryAutoFocus() {
try {
if (camera != null) camera.autoFocus(this);
} catch (RuntimeException e) {
LOG.log(WARNING, "Error retrying auto focus", e);
}
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.android.util;
import android.hardware.Camera;
@SuppressWarnings("deprecation")
public interface PreviewConsumer {
void start(Camera camera);
void stop();
}

View File

@@ -0,0 +1,100 @@
package org.briarproject.android.util;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.os.AsyncTask;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
@SuppressWarnings("deprecation")
public class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
private static final Logger LOG =
Logger.getLogger(QrCodeDecoder.class.getName());
private final Reader reader = new QRCodeReader();
private final ResultCallback callback;
private boolean stopped = false;
public QrCodeDecoder(ResultCallback callback) {
this.callback = callback;
}
public void start(Camera camera) {
stopped = false;
askForPreviewFrame(camera);
}
public void stop() {
stopped = true;
}
private void askForPreviewFrame(Camera camera) {
if (!stopped) camera.setOneShotPreviewCallback(this);
}
public void onPreviewFrame(byte[] data, Camera camera) {
if (!stopped) {
Size size = camera.getParameters().getPreviewSize();
new DecoderTask(camera, data, size.width, size.height).execute();
}
}
private class DecoderTask extends AsyncTask<Void, Void, Void> {
final Camera camera;
final byte[] data;
final int width, height;
DecoderTask(Camera camera, byte[] data, int width, int height) {
this.camera = camera;
this.data = data;
this.width = width;
this.height = height;
}
@Override
protected Void doInBackground(Void... params) {
long now = System.currentTimeMillis();
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
height, 0, 0, width, height, false);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
Result result = null;
try {
result = reader.decode(bitmap);
} catch (ReaderException e) {
return null; // No barcode found
} finally {
reader.reset();
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Decoding barcode took " + duration + " ms");
callback.handleResult(result);
return null;
}
@Override
protected void onPostExecute(Void result) {
askForPreviewFrame(camera);
}
}
public interface ResultCallback {
void handleResult(Result result);
}
}

View File

@@ -0,0 +1,51 @@
package org.briarproject.android.util;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.DisplayMetrics;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
public class QrCodeUtils {
private static final Logger LOG =
Logger.getLogger(QrCodeUtils.class.getName());
public static Bitmap createQrCode(Activity activity, String input) {
// Get narrowest screen dimension
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
try {
// Generate QR code
final BitMatrix encoded = new QRCodeWriter().encode(
input, BarcodeFormat.QR_CODE, smallestDimen, smallestDimen);
// Convert QR code to Bitmap
int width = encoded.getWidth();
int height = encoded.getHeight();
int[] pixels = new int[width * height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pixels[y * width + x] =
encoded.get(x, y) ? Color.BLACK : Color.WHITE;
}
}
Bitmap qr = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
qr.setPixels(pixels, 0, width, 0, 0, width, height);
return qr;
} catch (WriterException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return null;
}
}
}

View File

@@ -14,6 +14,9 @@ import org.briarproject.api.TransportId;
import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.keyagreement.KeyAgreementConnection;
import org.briarproject.api.keyagreement.KeyAgreementListener;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.plugins.Backoff;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -67,6 +70,9 @@ class DroidtoothPlugin implements DuplexPlugin {
private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private static final String PROP_ADDRESS = "address";
private static final String PROP_UUID = "uuid";
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final Context appContext;
@@ -161,7 +167,7 @@ class DroidtoothPlugin implements DuplexPlugin {
if (!StringUtils.isNullOrEmpty(address)) {
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put("address", address);
p.put(PROP_ADDRESS, address);
callback.mergeLocalProperties(p);
}
// Bind a server socket to accept connections from contacts
@@ -187,13 +193,13 @@ class DroidtoothPlugin implements DuplexPlugin {
}
private UUID getUuid() {
String uuid = callback.getLocalProperties().get("uuid");
String uuid = callback.getLocalProperties().get(PROP_UUID);
if (uuid == null) {
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put("uuid", uuid);
p.put(PROP_UUID, uuid);
callback.mergeLocalProperties(p);
}
return UUID.fromString(uuid);
@@ -264,9 +270,9 @@ class DroidtoothPlugin implements DuplexPlugin {
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
final ContactId c = e.getKey();
if (connected.contains(c)) continue;
final String address = e.getValue().get("address");
final String address = e.getValue().get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get("uuid");
final String uuid = e.getValue().get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(new Runnable() {
public void run() {
@@ -325,9 +331,9 @@ class DroidtoothPlugin implements DuplexPlugin {
if (!isRunning()) return null;
TransportProperties p = callback.getRemoteProperties().get(c);
if (p == null) return null;
String address = p.get("address");
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get("uuid");
String uuid = p.get(PROP_UUID);
if (StringUtils.isNullOrEmpty(uuid)) return null;
BluetoothSocket s = connect(address, uuid);
if (s == null) return null;
@@ -417,6 +423,48 @@ class DroidtoothPlugin implements DuplexPlugin {
});
}
public boolean supportsKeyAgreement() {
return true;
}
public KeyAgreementListener createKeyAgreementListener(
byte[] localCommitment) {
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(localCommitment);
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
// Bind a server socket for receiving invitation connections
BluetoothServerSocket ss;
try {
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
}
TransportProperties p = new TransportProperties();
String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
if (!StringUtils.isNullOrEmpty(address))
p.put(PROP_ADDRESS, address);
TransportDescriptor d = new TransportDescriptor(ID, p);
return new BluetoothKeyAgreementListener(d, ss);
}
public DuplexTransportConnection createKeyAgreementConnection(
byte[] remoteCommitment, TransportDescriptor d, long timeout) {
if (!isRunning()) return null;
if (!ID.equals(d.getIdentifier())) return null;
TransportProperties p = d.getProperties();
if (p == null) return null;
String address = p.get(PROP_ADDRESS);
if (StringUtils.isNullOrEmpty(address)) return null;
// No truncation necessary because COMMIT_LENGTH = 16
UUID uuid = UUID.nameUUIDFromBytes(remoteCommitment);
if (LOG.isLoggable(INFO))
LOG.info("Connecting to key agreement UUID " + uuid);
BluetoothSocket s = connect(address, uuid.toString());
if (s == null) return null;
return new DroidtoothTransportConnection(this, s);
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
@@ -545,4 +593,39 @@ class DroidtoothPlugin implements DuplexPlugin {
return s;
}
}
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
private final BluetoothServerSocket ss;
public BluetoothKeyAgreementListener(TransportDescriptor descriptor,
BluetoothServerSocket ss) {
super(descriptor);
this.ss = ss;
}
@Override
public Callable<KeyAgreementConnection> listen() {
return new Callable<KeyAgreementConnection>() {
@Override
public KeyAgreementConnection call() throws IOException {
BluetoothSocket s = ss.accept();
if (LOG.isLoggable(INFO))
LOG.info(ID.getString() + ": Incoming connection");
return new KeyAgreementConnection(
new DroidtoothTransportConnection(
DroidtoothPlugin.this, s), ID);
}
};
}
@Override
public void close() {
try {
ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
}
}

View File

@@ -19,6 +19,8 @@ import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.SettingsUpdatedEvent;
import org.briarproject.api.keyagreement.KeyAgreementListener;
import org.briarproject.api.keyagreement.TransportDescriptor;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
@@ -570,6 +572,20 @@ class TorPlugin implements DuplexPlugin, EventHandler,
throw new UnsupportedOperationException();
}
public boolean supportsKeyAgreement() {
return false;
}
public KeyAgreementListener createKeyAgreementListener(
byte[] commitment) {
throw new UnsupportedOperationException();
}
public DuplexTransportConnection createKeyAgreementConnection(
byte[] commitment, TransportDescriptor d, long timeout) {
throw new UnsupportedOperationException();
}
public void circuitStatus(String status, String id, String path) {
if (status.equals("BUILT") && !circuitBuilt.getAndSet(true)) {
LOG.info("First circuit built");