mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
Implement BQP Android UI using QR codes
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
briar-android/res/layout/fragment_keyagreement_qr.xml
Normal file
59
briar-android/res/layout/fragment_keyagreement_qr.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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>
|
||||
@@ -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) {
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(), 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
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.QrCodeUtils;
|
||||
import org.briarproject.android.util.QrCodeDecoder;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.KeyAgreementAbortedEvent;
|
||||
import org.briarproject.api.event.KeyAgreementFailedEvent;
|
||||
import org.briarproject.api.event.KeyAgreementFinishedEvent;
|
||||
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.concurrent.atomic.AtomicBoolean;
|
||||
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;
|
||||
|
||||
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 volatile KeyAgreementTask task;
|
||||
private volatile boolean toggleBluetooth;
|
||||
private volatile BluetoothAdapter adapter;
|
||||
private BluetoothStateReceiver receiver;
|
||||
private AtomicBoolean waitingForBluetooth = new AtomicBoolean();
|
||||
private QrCodeDecoder decoder;
|
||||
private boolean gotRemotePayload;
|
||||
|
||||
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);
|
||||
|
||||
// Only enable BT adapter if it is not already on.
|
||||
adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (adapter != null)
|
||||
toggleBluetooth = !adapter.isEnabled();
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
if (adapter != null && toggleBluetooth) {
|
||||
waitingForBluetooth.set(true);
|
||||
toggleBluetooth(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();
|
||||
if (toggleBluetooth) toggleBluetooth(false);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void toggleBluetooth(boolean enable) {
|
||||
if (adapter != null) {
|
||||
AndroidUtils.enableBluetooth(adapter, enable);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
} else if (e instanceof KeyAgreementFinishedEvent) {
|
||||
// We want to reuse the connection, so don't disable Bluetooth
|
||||
toggleBluetooth = false;
|
||||
}
|
||||
}
|
||||
|
||||
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.get()) {
|
||||
LOG.info("Bluetooth enabled");
|
||||
waitingForBluetooth.set(false);
|
||||
startListening();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
258
briar-android/src/org/briarproject/android/util/CameraView.java
Normal file
258
briar-android/src/org/briarproject/android/util/CameraView.java
Normal file
@@ -0,0 +1,258 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
camera.setDisplayOrientation(orientation);
|
||||
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();
|
||||
Parameters params = camera.getParameters();
|
||||
setPreviewSize(params);
|
||||
applyParameters(params);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.hardware.Camera;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public interface PreviewConsumer {
|
||||
|
||||
void start(Camera camera);
|
||||
|
||||
void stop();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,14 @@ dependencies {
|
||||
compile fileTree(dir: 'libs', include: '*.jar')
|
||||
compile "com.madgag.spongycastle:core:1.54.0.0"
|
||||
compile "com.h2database:h2:1.4.190"
|
||||
compile "com.google.zxing:core:3.2.1"
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
verify = [
|
||||
'com.madgag.spongycastle:core:1e7fa4b19ccccd1011364ab838d0b4702470c178bbbdd94c5c90b2d4d749ea1e',
|
||||
'com.h2database:h2:23ba495a07bbbb3bd6c3084d10a96dad7a23741b8b6d64b213459a784195a98c',
|
||||
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user