Commit 8cacc73b authored by str4d's avatar str4d

Implement BQP Android UI using QR codes

parent 701cfdba
......@@ -14,11 +14,13 @@
/>
<uses-feature android:name="android.hardware.bluetooth"/>
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.VIBRATE" />
......@@ -165,6 +167,16 @@
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.keyagreement.KeyAgreementActivity"
android:label="@string/add_contact_title"
android:theme="@style/BriarThemeNoActionBar.Default"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.StartupFailureActivity"
android:label="@string/startup_failed_activity_title">
......
......@@ -46,7 +46,7 @@ dependencyVerification {
'com.android.support:recyclerview-v7:7606373da0931a1e62588335465a0e390cd676c98117edab29220317495faefd',
'info.guardianproject.panic:panic:a7ed9439826db2e9901649892cf9afbe76f00991b768d8f4c26332d7c9406cb2',
'info.guardianproject.trustedintents:trustedintents:6221456d8821a8d974c2acf86306900237cf6afaaa94a4c9c44e161350f80f3e',
'com.android.support:support-annotations:f347a35b9748a4103b39a6714a77e2100f488d623fd6268e259c177b200e9d82'
'de.hdodenhof:circleimageview:c76d936395b50705a3f98c9220c22d2599aeb9e609f559f6048975cfc1f686b8',
]
}
......@@ -97,4 +97,4 @@ android {
lintOptions {
abortOnError false
}
}
\ No newline at end of file
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/qr_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/black"
android:gravity="center">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium">
<ProgressBar
style="?android:attr/progressBarStyleInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/margin_large"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</RelativeLayout>
<org.briarproject.android.util.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"/>
</LinearLayout>
\ No newline at end of file
......@@ -34,6 +34,7 @@
<string name="contact_list_title">Contacts</string>
<string name="no_contacts">No contacts</string>
<string name="add_contact_title">Add a Contact</string>
<string name="add_contact_title_step">Add a Contact - Step %1$d/%2$d</string>
<string name="your_nickname">Choose the identity you want to use:</string>
<string name="face_to_face">You must be face-to-face with the person you want to add as a contact. This will prevent anyone from impersonating you or reading your messages in future.</string>
<string name="continue_button">Continue</string>
......@@ -52,6 +53,14 @@
<string name="codes_do_not_match">Codes do not match</string>
<string name="interfering">This could mean that someone is trying to interfere with your connection</string>
<string name="contact_added_toast">Contact added: %s</string>
<string name="contact_already_exists">Contact %s already exists</string>
<string name="contact_exchange_failed">Contact exchange failed</string>
<string name="scan_qr_code">Scan QR code</string>
<string name="qr_code_invalid">The QR code is invalid</string>
<string name="connecting_to_device">Connecting to device\u2026</string>
<string name="authenticating_with_device">Authenticating with device\u2026</string>
<string name="connection_aborted_local">Connection aborted by us! This could mean that someone is trying to interfere with your connection</string>
<string name="connection_aborted_remote">Connection aborted by your contact! This could mean that someone is trying to interfere with your connection</string>
<string name="no_private_messages">No messages</string>
<string name="private_message_hint">Type message</string>
<string name="message_sent_toast">Message sent</string>
......
......@@ -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);
......
......@@ -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) {
/*
......
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);
}
}
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(), true);
}
});
}
@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();
}
});
}
}
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());
}