diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 0f483f12b..3c3ac063e 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -300,7 +300,7 @@ diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 68564c4e4..1d92a31f3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -26,9 +26,10 @@ import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.IntroductionActivity; 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; @@ -100,6 +101,8 @@ public interface ActivityComponent { void inject(PanicPreferencesActivity activity); + void inject(ContactExchangeActivity activity); + void inject(KeyAgreementActivity activity); void inject(ConversationActivity activity); @@ -185,7 +188,7 @@ public interface ActivityComponent { void inject(IntroFragment fragment); - void inject(ShowQrCodeFragment fragment); + void inject(KeyAgreementFragment fragment); void inject(ContactChooserFragment fragment); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java index 3b3265071..2dc00d67f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java @@ -33,7 +33,7 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener; import org.briarproject.briar.android.fragment.BaseFragment; -import org.briarproject.briar.android.keyagreement.KeyAgreementActivity; +import org.briarproject.briar.android.keyagreement.ContactExchangeActivity; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.client.BaseMessageHeader; @@ -165,7 +165,7 @@ public class ContactListFragment extends BaseFragment implements EventListener { switch (item.getItemId()) { case R.id.action_add_contact: Intent intent = - new Intent(getContext(), KeyAgreementActivity.class); + new Intent(getContext(), ContactExchangeActivity.class); startActivity(intent); return true; default: diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java index aa6ba31ac..d9cf8d01f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java @@ -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"); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java new file mode 100644 index 000000000..e053c50c1 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java @@ -0,0 +1,144 @@ +package org.briarproject.briar.android.keyagreement; + +import android.os.Bundle; +import android.support.annotation.UiThread; +import android.widget.Toast; + +import org.briarproject.bramble.api.contact.ContactExchangeListener; +import org.briarproject.bramble.api.contact.ContactExchangeTask; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.keyagreement.KeyAgreementResult; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.R.string; +import org.briarproject.briar.android.activity.ActivityComponent; + +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.logException; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ContactExchangeActivity extends KeyAgreementActivity implements + ContactExchangeListener { + + private static final Logger LOG = + Logger.getLogger(ContactExchangeActivity.class.getName()); + + // Fields that are accessed from background threads must be volatile + @Inject + volatile ContactExchangeTask contactExchangeTask; + @Inject + volatile IdentityManager identityManager; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + getSupportActionBar().setTitle(string.add_contact_title); + } + + private void startContactExchange(KeyAgreementResult result) { + runOnDbThread(() -> { + LocalAuthor localAuthor; + // Load the local pseudonym + try { + localAuthor = identityManager.getLocalAuthor(); + } catch (DbException e) { + logException(LOG, WARNING, e); + contactExchangeFailed(); + return; + } + + // Exchange contact details + contactExchangeTask.startExchange(ContactExchangeActivity.this, + localAuthor, result.getMasterKey(), + result.getConnection(), result.getTransportId(), + result.wasAlice()); + }); + } + + @Override + public void contactExchangeSucceeded(Author remoteAuthor) { + runOnUiThreadUnlessDestroyed(() -> { + String contactName = remoteAuthor.getName(); + String format = getString(string.contact_added_toast); + String text = String.format(format, contactName); + Toast.makeText(ContactExchangeActivity.this, text, LENGTH_LONG) + .show(); + supportFinishAfterTransition(); + }); + } + + @Override + public void duplicateContact(Author remoteAuthor) { + runOnUiThreadUnlessDestroyed(() -> { + String contactName = remoteAuthor.getName(); + String format = getString(string.contact_already_exists); + String text = String.format(format, contactName); + Toast.makeText(ContactExchangeActivity.this, text, LENGTH_LONG) + .show(); + finish(); + }); + } + + @Override + public void contactExchangeFailed() { + runOnUiThreadUnlessDestroyed(() -> { + Toast.makeText(ContactExchangeActivity.this, + string.contact_exchange_failed, LENGTH_LONG).show(); + finish(); + }); + } + + @UiThread + @Override + public void keyAgreementFailed() { + // TODO show failure somewhere persistent? + Toast.makeText(this, R.string.connection_failed, + LENGTH_LONG).show(); + } + + @UiThread + @Override + public String keyAgreementWaiting() { + return getString(R.string.waiting_for_contact_to_scan); + } + + @UiThread + @Override + public String keyAgreementStarted() { + return getString(R.string.authenticating_with_device); + } + + @UiThread + @Override + public String keyAgreementAborted(boolean remoteAborted) { + // TODO show abort somewhere persistent? + Toast.makeText(this, + remoteAborted ? R.string.connection_aborted_remote : + R.string.connection_aborted_local, LENGTH_LONG) + .show(); + return null; + } + + @UiThread + @Override + public String keyAgreementFinished(KeyAgreementResult result) { + startContactExchange(result); + return getString(string.exchanging_contact_details); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java index c64841dd3..5f722eb33 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java @@ -16,17 +16,7 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import android.widget.Toast; -import org.briarproject.bramble.api.contact.ContactExchangeListener; -import org.briarproject.bramble.api.contact.ContactExchangeTask; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.event.EventListener; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.identity.LocalAuthor; -import org.briarproject.bramble.api.keyagreement.KeyAgreementResult; -import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent; @@ -53,16 +43,14 @@ import static android.bluetooth.BluetoothAdapter.STATE_ON; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION.SDK_INT; import static android.widget.Toast.LENGTH_LONG; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ENABLE_BLUETOOTH; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA; @MethodsNotNullByDefault @ParametersNotNullByDefault -public class KeyAgreementActivity extends BriarActivity implements - BaseFragmentListener, IntroScreenSeenListener, EventListener, - ContactExchangeListener { +public abstract class KeyAgreementActivity extends BriarActivity implements + BaseFragmentListener, IntroScreenSeenListener, + KeyAgreementFragment.KeyAgreementEventListener { private enum BluetoothState { UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED @@ -74,12 +62,6 @@ public class KeyAgreementActivity extends BriarActivity implements @Inject EventBus eventBus; - // Fields that are accessed from background threads must be volatile - @Inject - volatile ContactExchangeTask contactExchangeTask; - @Inject - volatile IdentityManager identityManager; - private boolean isResumed = false, enableWasRequested = false; private boolean continueClicked, gotCameraPermission; private BluetoothState bluetoothState = BluetoothState.UNKNOWN; @@ -95,13 +77,9 @@ public class KeyAgreementActivity extends BriarActivity implements public void onCreate(@Nullable Bundle state) { super.onCreate(state); setContentView(R.layout.activity_fragment_container_toolbar); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - getSupportActionBar().setTitle(R.string.add_contact_title); if (state == null) { showInitialFragment(IntroFragment.newInstance()); } @@ -116,18 +94,6 @@ public class KeyAgreementActivity extends BriarActivity implements if (bluetoothReceiver != null) unregisterReceiver(bluetoothReceiver); } - @Override - public void onStart() { - super.onStart(); - eventBus.addListener(this); - } - - @Override - protected void onStop() { - super.onStop(); - eventBus.removeListener(this); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -148,7 +114,7 @@ public class KeyAgreementActivity extends BriarActivity implements if (canShowQrCodeFragment()) showQrCodeFragment(); } - boolean canShowQrCodeFragment() { + private boolean canShowQrCodeFragment() { return isResumed && continueClicked && (SDK_INT < 23 || gotCameraPermission) && bluetoothState != BluetoothState.UNKNOWN @@ -207,10 +173,11 @@ public class KeyAgreementActivity extends BriarActivity implements } private void showQrCodeFragment() { + 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(this); fm.beginTransaction() .replace(R.id.fragmentContainer, f, f.getUniqueTag()) .addToBackStack(f.getUniqueTag()) @@ -280,69 +247,6 @@ public class KeyAgreementActivity extends BriarActivity implements } } - @Override - public void eventOccurred(Event e) { - if (e instanceof KeyAgreementFinishedEvent) { - KeyAgreementFinishedEvent event = (KeyAgreementFinishedEvent) e; - keyAgreementFinished(event.getResult()); - } - } - - private void keyAgreementFinished(KeyAgreementResult result) { - runOnUiThreadUnlessDestroyed(() -> startContactExchange(result)); - } - - private void startContactExchange(KeyAgreementResult result) { - runOnDbThread(() -> { - LocalAuthor localAuthor; - // Load the local pseudonym - try { - localAuthor = identityManager.getLocalAuthor(); - } catch (DbException e) { - logException(LOG, WARNING, e); - contactExchangeFailed(); - return; - } - - // Exchange contact details - contactExchangeTask.startExchange(KeyAgreementActivity.this, - localAuthor, result.getMasterKey(), - result.getConnection(), result.getTransportId(), - result.wasAlice()); - }); - } - - @Override - public void contactExchangeSucceeded(Author remoteAuthor) { - runOnUiThreadUnlessDestroyed(() -> { - String contactName = remoteAuthor.getName(); - String format = getString(string.contact_added_toast); - String text = String.format(format, contactName); - Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG).show(); - supportFinishAfterTransition(); - }); - } - - @Override - public void duplicateContact(Author remoteAuthor) { - runOnUiThreadUnlessDestroyed(() -> { - String contactName = remoteAuthor.getName(); - String format = getString(string.contact_already_exists); - String text = String.format(format, contactName); - Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG).show(); - finish(); - }); - } - - @Override - public void contactExchangeFailed() { - runOnUiThreadUnlessDestroyed(() -> { - Toast.makeText(KeyAgreementActivity.this, - string.contact_exchange_failed, LENGTH_LONG).show(); - finish(); - }); - } - private class BluetoothStateReceiver extends BroadcastReceiver { @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java similarity index 67% rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java rename to briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java index 2c38043fb..6781b88b6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java @@ -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; @@ -24,6 +18,7 @@ import com.google.zxing.Result; import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.keyagreement.KeyAgreementResult; import org.briarproject.bramble.api.keyagreement.KeyAgreementTask; import org.briarproject.bramble.api.keyagreement.Payload; import org.briarproject.bramble.api.keyagreement.PayloadEncoder; @@ -36,11 +31,13 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; 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 +60,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 +81,21 @@ 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; + private KeyAgreementEventListener listener; - public static ShowQrCodeFragment newInstance() { - + public static KeyAgreementFragment newInstance( + KeyAgreementEventListener listener) { Bundle args = new Bundle(); - - ShowQrCodeFragment fragment = new ShowQrCodeFragment(); + KeyAgreementFragment fragment = new KeyAgreementFragment(); + fragment.listener = listener; fragment.setArguments(args); return fragment; } @@ -118,7 +115,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 +122,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 +141,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 setFullscreen(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 +213,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,84 +270,60 @@ 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(((KeyAgreementFinishedEvent) e).getResult()); } } - @UiThread - private void generateBitmapQR(Payload payload) { - // Get narrowest screen dimension - Context context = getContext(); - if (context == null) return; - DisplayMetrics dm = context.getResources().getDisplayMetrics(); - new AsyncTask() { - - @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(); - // TODO show failure somewhere persistent? - Toast.makeText(getActivity(), R.string.connection_failed, - LENGTH_LONG).show(); + listener.keyAgreementFailed(); }); } private void keyAgreementWaiting() { runOnUiThreadUnlessDestroyed( - () -> status.setText(R.string.waiting_for_contact_to_scan)); + () -> status.setText(listener.keyAgreementWaiting())); } private void keyAgreementStarted() { runOnUiThreadUnlessDestroyed(() -> { - qrCodeContainer.setVisibility(INVISIBLE); + qrCodeView.setVisibility(INVISIBLE); statusView.setVisibility(VISIBLE); - status.setText(R.string.authenticating_with_device); + status.setText(listener.keyAgreementStarted()); }); } private void keyAgreementAborted(boolean remoteAborted) { runOnUiThreadUnlessDestroyed(() -> { reset(); - qrCodeContainer.setVisibility(VISIBLE); + qrCodeView.setVisibility(VISIBLE); statusView.setVisibility(INVISIBLE); - status.setText(null); - // TODO show abort somewhere persistent? - Toast.makeText(getActivity(), - remoteAborted ? R.string.connection_aborted_remote : - R.string.connection_aborted_local, LENGTH_LONG) - .show(); + status.setText(listener.keyAgreementAborted(remoteAborted)); + }); + } + + private void keyAgreementFinished(KeyAgreementResult result) { + runOnUiThreadUnlessDestroyed(() -> { + statusView.setVisibility(VISIBLE); + status.setText(listener.keyAgreementFinished(result)); + }); + } + + 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)); }); } @@ -394,4 +341,31 @@ public class ShowQrCodeFragment extends BaseEventFragment protected void finish() { getActivity().getSupportFragmentManager().popBackStack(); } + + @NotNullByDefault + interface KeyAgreementEventListener { + + @UiThread + void keyAgreementFailed(); + + // Should return a string to be displayed as status. + @UiThread + @Nullable + String keyAgreementWaiting(); + + // Should return a string to be displayed as status. + @UiThread + @Nullable + String keyAgreementStarted(); + + // Should return a string to be displayed as status. + @UiThread + @Nullable + String keyAgreementAborted(boolean remoteAborted); + + // Should return a string to be displayed as status. + @UiThread + @Nullable + String keyAgreementFinished(KeyAgreementResult result); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/QrCodeView.java b/briar-android/src/main/java/org/briarproject/briar/android/view/QrCodeView.java new file mode 100644 index 000000000..32b695622 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/QrCodeView.java @@ -0,0 +1,63 @@ +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.setFullscreen(fullscreen); + } + ); + } + + @UiThread + 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 setFullscreen(boolean fullscreen); + } + +} diff --git a/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml b/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml deleted file mode 100644 index 9d378e5ba..000000000 --- a/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml b/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml index d9527df8f..f992ee203 100644 --- a/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml +++ b/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml @@ -1,7 +1,6 @@ @@ -43,45 +42,11 @@ tools:text="Connection failed"/> - - - - - - - - - - - + android:background="@android:color/white"/> diff --git a/briar-android/src/main/res/layout/qr_code_view.xml b/briar-android/src/main/res/layout/qr_code_view.xml new file mode 100644 index 000000000..9062428d0 --- /dev/null +++ b/briar-android/src/main/res/layout/qr_code_view.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + +