Create a dedicated qrCodeView

This commit is contained in:
goapunk
2018-05-31 14:25:27 +02:00
parent 26643e491b
commit 06deba4bd4
8 changed files with 202 additions and 270 deletions

View File

@@ -29,7 +29,7 @@ import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
import org.briarproject.briar.android.keyagreement.IntroFragment;
import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
import org.briarproject.briar.android.login.AuthorNameFragment;
import org.briarproject.briar.android.login.ChangePasswordActivity;
import org.briarproject.briar.android.login.DozeFragment;
@@ -188,7 +188,7 @@ public interface ActivityComponent {
void inject(IntroFragment fragment);
void inject(ShowQrCodeFragment fragment);
void inject(KeyAgreementFragment fragment);
void inject(ContactChooserFragment fragment);

View File

@@ -9,10 +9,12 @@ import android.hardware.Camera.Size;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -21,6 +23,7 @@ import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import static android.content.Context.WINDOW_SERVICE;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
import static android.hardware.Camera.Parameters.FLASH_MODE_OFF;
@@ -97,7 +100,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
}
@UiThread
public void start(int rotationDegrees) throws CameraException {
public void start() throws CameraException {
LOG.info("Opening camera");
try {
int cameras = Camera.getNumberOfCameras();
@@ -122,7 +125,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} catch (RuntimeException e) {
throw new CameraException(e);
}
setDisplayOrientation(rotationDegrees);
setDisplayOrientation(getScreenRotationDegrees());
// Use barcode scene mode if it's available
Parameters params = camera.getParameters();
params = setSceneMode(camera, params);
@@ -157,6 +160,27 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
camera = null;
}
/**
* See {@link Camera#setDisplayOrientation(int)}.
*/
private int getScreenRotationDegrees() {
WindowManager wm =
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
Display d = wm.getDefaultDisplay();
switch (d.getRotation()) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
default:
throw new AssertionError();
}
}
@UiThread
private void startPreview(SurfaceHolder holder) throws CameraException {
LOG.info("Starting preview");

View File

@@ -193,8 +193,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
continueClicked = false;
// FIXME #824
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(ShowQrCodeFragment.TAG) == null) {
BaseFragment f = ShowQrCodeFragment.newInstance();
if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) {
BaseFragment f = KeyAgreementFragment.newInstance();
fm.beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.addToBackStack(f.getUniqueTag())

View File

@@ -2,18 +2,12 @@ package org.briarproject.briar.android.keyagreement;
import android.content.Context;
import android.graphics.Bitmap;
import android.hardware.Camera;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.UiThread;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
@@ -41,6 +35,7 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseEventFragment;
import org.briarproject.briar.android.fragment.ErrorFragment;
import org.briarproject.briar.android.view.QrCodeView;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -63,10 +58,10 @@ import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ShowQrCodeFragment extends BaseEventFragment
implements QrCodeDecoder.ResultCallback {
public class KeyAgreementFragment extends BaseEventFragment
implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener {
static final String TAG = ShowQrCodeFragment.class.getName();
static final String TAG = KeyAgreementFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
@@ -84,21 +79,18 @@ public class ShowQrCodeFragment extends BaseEventFragment
EventBus eventBus;
private CameraView cameraView;
private LinearLayout cameraOverlay;
private View statusView;
private QrCodeView qrCodeView;
private TextView status;
private View qrCodeContainer;
private ImageView qrCode;
private boolean fullscreen = false;
private boolean gotRemotePayload;
private volatile boolean gotLocalPayload;
private KeyAgreementTask task;
public static ShowQrCodeFragment newInstance() {
public static KeyAgreementFragment newInstance() {
Bundle args = new Bundle();
ShowQrCodeFragment fragment = new ShowQrCodeFragment();
KeyAgreementFragment fragment = new KeyAgreementFragment();
fragment.setArguments(args);
return fragment;
}
@@ -118,7 +110,6 @@ public class ShowQrCodeFragment extends BaseEventFragment
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
false);
}
@@ -126,45 +117,17 @@ public class ShowQrCodeFragment extends BaseEventFragment
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
cameraView = view.findViewById(R.id.camera_view);
cameraOverlay = view.findViewById(R.id.camera_overlay);
statusView = view.findViewById(R.id.status_container);
status = view.findViewById(R.id.connect_status);
qrCodeContainer = view.findViewById(R.id.qr_code_container);
qrCode = view.findViewById(R.id.qr_code);
ImageView fullscreenButton = view.findViewById(R.id.fullscreen_button);
fullscreenButton.setOnClickListener(v -> {
LinearLayout cameraOverlay = view.findViewById(R.id.camera_overlay);
LayoutParams statusParams, qrCodeParams;
if (fullscreen) {
// Shrink the QR code container to fill half its parent
if (cameraOverlay.getOrientation() == HORIZONTAL) {
statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
} else {
statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
}
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_black_48dp);
} else {
// Grow the QR code container to fill its parent
statusParams = new LayoutParams(0, 0, 0f);
qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_exit_black_48dp);
}
statusView.setLayoutParams(statusParams);
qrCodeContainer.setLayoutParams(qrCodeParams);
cameraOverlay.invalidate();
fullscreen = !fullscreen;
});
qrCodeView = view.findViewById(R.id.qr_code_view);
qrCodeView.setFullscreenListener(this);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
cameraView.setPreviewConsumer(new QrCodeDecoder(this));
}
@@ -173,30 +136,33 @@ public class ShowQrCodeFragment extends BaseEventFragment
public void onStart() {
super.onStart();
try {
cameraView.start(getScreenRotationDegrees());
cameraView.start();
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
}
startListening();
}
/**
* See {@link Camera#setDisplayOrientation(int)}.
*/
private int getScreenRotationDegrees() {
Display d = getActivity().getWindowManager().getDefaultDisplay();
switch (d.getRotation()) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
default:
throw new AssertionError();
@Override
public void toggleFullscreen(boolean fullscreen) {
LinearLayout.LayoutParams statusParams, qrCodeParams;
if (fullscreen) {
// Grow the QR code view to fill its parent
statusParams = new LayoutParams(0, 0, 0f);
qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
} else {
// Shrink the QR code view to fill half its parent
if (cameraOverlay.getOrientation() == HORIZONTAL) {
statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
} else {
statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
}
}
statusView.setLayoutParams(statusParams);
qrCodeView.setLayoutParams(qrCodeParams);
cameraOverlay.invalidate();
}
@Override
@@ -242,7 +208,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
// If we've stopped the camera view, restart it
if (gotRemotePayload) {
try {
cameraView.start(getScreenRotationDegrees());
cameraView.start();
} catch (CameraException e) {
logCameraExceptionAndFinish(e);
return;
@@ -299,51 +265,10 @@ public class ShowQrCodeFragment extends BaseEventFragment
KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e;
keyAgreementAborted(event.didRemoteAbort());
} else if (e instanceof KeyAgreementFinishedEvent) {
runOnUiThreadUnlessDestroyed(() -> {
statusView.setVisibility(VISIBLE);
status.setText(R.string.exchanging_contact_details);
});
keyAgreementFinished();
}
}
@UiThread
private void generateBitmapQR(Payload payload) {
// Get narrowest screen dimension
Context context = getContext();
if (context == null) return;
DisplayMetrics dm = context.getResources().getDisplayMetrics();
new AsyncTask<Void, Void, Bitmap>() {
@Override
@Nullable
protected Bitmap doInBackground(Void... params) {
byte[] payloadBytes = payloadEncoder.encode(payload);
if (LOG.isLoggable(INFO)) {
LOG.info("Local payload is " + payloadBytes.length
+ " bytes");
}
// Use ISO 8859-1 to encode bytes directly as a string
String content = new String(payloadBytes, ISO_8859_1);
return QrCodeUtils.createQrCode(dm, content);
}
@Override
protected void onPostExecute(@Nullable Bitmap bitmap) {
if (bitmap != null && !isDetached()) {
qrCode.setImageBitmap(bitmap);
// Simple fade-in animation
AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
anim.setDuration(200);
qrCode.startAnimation(anim);
}
}
}.execute();
}
private void setQrCode(Payload localPayload) {
runOnUiThreadUnlessDestroyed(() -> generateBitmapQR(localPayload));
}
private void keyAgreementFailed() {
runOnUiThreadUnlessDestroyed(() -> {
reset();
@@ -353,25 +278,25 @@ public class ShowQrCodeFragment extends BaseEventFragment
});
}
private void keyAgreementWaiting() {
runOnUiThreadUnlessDestroyed(
() -> status.setText(R.string.waiting_for_contact_to_scan));
}
private void keyAgreementStarted() {
runOnUiThreadUnlessDestroyed(() -> {
qrCodeContainer.setVisibility(INVISIBLE);
qrCodeView.setVisibility(INVISIBLE);
statusView.setVisibility(VISIBLE);
status.setText(R.string.authenticating_with_device);
});
}
private void keyAgreementWaiting() {
runOnUiThreadUnlessDestroyed(
() -> status.setText(R.string.waiting_for_contact_to_scan));
}
private void keyAgreementAborted(boolean remoteAborted) {
runOnUiThreadUnlessDestroyed(() -> {
reset();
qrCodeContainer.setVisibility(VISIBLE);
qrCodeView.setVisibility(VISIBLE);
statusView.setVisibility(INVISIBLE);
status.setText(null);
status.setText("");
// TODO show abort somewhere persistent?
Toast.makeText(getActivity(),
remoteAborted ? R.string.connection_aborted_remote :
@@ -380,6 +305,30 @@ public class ShowQrCodeFragment extends BaseEventFragment
});
}
private void keyAgreementFinished() {
runOnUiThreadUnlessDestroyed(() -> {
statusView.setVisibility(VISIBLE);
status.setText(R.string.exchanging_contact_details);
});
}
private void setQrCode(Payload localPayload) {
Context context = getContext();
if (context == null) return;
DisplayMetrics dm = context.getResources().getDisplayMetrics();
ioExecutor.execute(() -> {
byte[] payloadBytes = payloadEncoder.encode(localPayload);
if (LOG.isLoggable(INFO)) {
LOG.info("Local payload is " + payloadBytes.length
+ " bytes");
}
// Use ISO 8859-1 to encode bytes directly as a string
String content = new String(payloadBytes, ISO_8859_1);
Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
runOnUiThreadUnlessDestroyed(() -> qrCodeView.setQrCode(qrCode));
});
}
@Override
public void handleResult(Result result) {
runOnUiThreadUnlessDestroyed(() -> {

View File

@@ -0,0 +1,61 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.animation.AlphaAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.briarproject.briar.R;
public class QrCodeView extends FrameLayout {
private final ImageView qrCodeImageView;
private boolean fullscreen = false;
private FullscreenListener listener;
public QrCodeView(@NonNull Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.qr_code_view, this, true);
qrCodeImageView = findViewById(R.id.qr_code);
ImageView fullscreenButton = findViewById(R.id.fullscreen_button);
fullscreenButton.setOnClickListener(v -> {
fullscreen = !fullscreen;
if (!fullscreen)
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_black_48dp);
else
fullscreenButton.setImageResource(
R.drawable.ic_fullscreen_exit_black_48dp);
if (listener != null)
listener.toggleFullscreen(fullscreen);
}
);
}
public void setQrCode(Bitmap qrCode) {
qrCodeImageView.setImageBitmap(qrCode);
// Simple fade-in animation
AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
anim.setDuration(200);
qrCodeImageView.startAnimation(anim);
}
@UiThread
public void setFullscreenListener(FullscreenListener listener) {
this.listener = listener;
}
public interface FullscreenListener {
void toggleFullscreen(boolean isFullscreen);
}
}

View File

@@ -1,110 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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">
<org.briarproject.briar.android.keyagreement.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:id="@+id/camera_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/status_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/background_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<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>
<FrameLayout
android:id="@+id/qr_code_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/white">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/fullscreen_button"
android:background="?selectableItemBackground"
android:src="@drawable/ic_fullscreen_black_48dp"
android:alpha="0.54"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/show_qr_code_fullscreen"/>
</RelativeLayout>
</FrameLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/container_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:visibility="invisible">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/title_progress_bar"
android:layout_centerHorizontal="true"/>
<TextView
android:id="@+id/title_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout>
</FrameLayout>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -43,45 +42,11 @@
tools:text="Connection failed"/>
</LinearLayout>
<FrameLayout
android:id="@+id/qr_code_container"
<org.briarproject.briar.android.view.QrCodeView
android:id="@+id/qr_code_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/white">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"
tools:src="@drawable/startup_lock"/>
<ImageView
android:id="@+id/fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_margin="@dimen/margin_small"
android:alpha="0.54"
android:background="?selectableItemBackground"
android:contentDescription="@string/show_qr_code_fullscreen"
android:src="@drawable/ic_fullscreen_black_48dp"/>
</RelativeLayout>
</FrameLayout>
android:background="@android:color/white"/>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="@layout/list_item_forum">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/qr_code"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/startup_lock"/>
<ImageView
android:id="@+id/fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:alpha="0.54"
android:background="?selectableItemBackground"
android:contentDescription="@string/show_qr_code_fullscreen"
android:src="@drawable/ic_fullscreen_black_48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>
</merge>