diff --git a/repeater-android/build.gradle b/repeater-android/build.gradle index ef5439780326aacf247c73f9b822aeea258493b2..0f8df15ccd75a6c0c0bd270afec2979ad4e3d76e 100644 --- a/repeater-android/build.gradle +++ b/repeater-android/build.gradle @@ -40,6 +40,7 @@ dependencies { def supportVersion = '27.1.1' compileOnly 'javax.annotation:jsr250-api:1.0' kapt "com.google.dagger:dagger-compiler:2.0.2" + implementation 'com.google.zxing:core:3.3.0' implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:support-v4:$supportVersion" implementation ("com.android.support:design:$supportVersion") { diff --git a/repeater-android/src/main/AndroidManifest.xml b/repeater-android/src/main/AndroidManifest.xml index efb3f93848c1ac82047bccb236bd61b91ebc9b52..1afeed2c670ed0b57c04f3f353ce4cca821820bc 100644 --- a/repeater-android/src/main/AndroidManifest.xml +++ b/repeater-android/src/main/AndroidManifest.xml @@ -2,6 +2,22 @@ <manifest package="org.briarproject.briar.repeater" xmlns:android="http://schemas.android.com/apk/res/android"> + <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.VIBRATE"/> + <uses-permission android:name="android.permission.WAKE_LOCK"/> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> + + <uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> + + <application android:name="org.briarproject.repeater.RepeaterApplication" android:allowBackup="false" @@ -19,6 +35,16 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity + android:name="org.briarproject.repeater.keyagreement.ContactExchangeActivity" + android:label="@string/add_contact_title" + android:parentActivityName="org.briarproject.repeater.activity.NavDrawerActivity" + android:theme="@style/BriarTheme.NoActionBar"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="org.briarproject.repeater.activity.NavDrawerActivity"/> + </activity> + <service android:name="org.briarproject.repeater.RepeaterService" android:exported="false"> diff --git a/repeater-android/src/main/java/org/briarproject/repeater/activity/BriarActivity.java b/repeater-android/src/main/java/org/briarproject/repeater/activity/BriarActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..ce20beef19bbc3a7025c5f19043a8cfbae3789bc --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/activity/BriarActivity.java @@ -0,0 +1,62 @@ +package org.briarproject.repeater.activity; + +import android.annotation.SuppressLint; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; + + +import org.briarproject.briar.repeater.R; + +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static android.os.Build.VERSION.SDK_INT; + +@SuppressLint("Registered") +public abstract class BriarActivity extends BaseActivity { + + public static final String GROUP_ID = "briar.GROUP_ID"; + public static final String GROUP_NAME = "briar.GROUP_NAME"; + + private static final Logger LOG = + Logger.getLogger(BriarActivity.class.getName()); + + /** + * This should be called after the content view has been added in onCreate() + * + * @param ownLayout true if the custom toolbar brings its own layout + * @return the Toolbar object or null if content view did not contain one + */ + @Nullable + protected Toolbar setUpCustomToolbar(boolean ownLayout) { + // Custom Toolbar + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setDisplayShowHomeEnabled(true); + ab.setDisplayHomeAsUpEnabled(true); + ab.setDisplayShowCustomEnabled(ownLayout); + ab.setDisplayShowTitleEnabled(!ownLayout); + } + return toolbar; + } + + + private void exit(boolean removeFromRecentApps) { + finishAndExit(); + } + + private void finishAndExit() { + if (SDK_INT >= 21) finishAndRemoveTask(); + else supportFinishAfterTransition(); + LOG.info("Exiting"); + System.exit(0); + } + + @Deprecated + protected void finishOnUiThread() { + runOnUiThreadUnlessDestroyed(this::supportFinishAfterTransition); + } +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/activity/RequestCodes.java b/repeater-android/src/main/java/org/briarproject/repeater/activity/RequestCodes.java new file mode 100644 index 0000000000000000000000000000000000000000..62b9ed96205d9abed00f013ceb7a18144dcb5c1c --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/activity/RequestCodes.java @@ -0,0 +1,16 @@ +package org.briarproject.repeater.activity; + +public interface RequestCodes { + + int REQUEST_PASSWORD = 1; + int REQUEST_INTRODUCTION = 2; + int REQUEST_GROUP_INVITE = 3; + int REQUEST_SHARE_FORUM = 4; + int REQUEST_WRITE_BLOG_POST = 5; + int REQUEST_SHARE_BLOG = 6; + int REQUEST_RINGTONE = 7; + int REQUEST_PERMISSION_CAMERA = 8; + int REQUEST_DOZE_WHITELISTING = 9; + int REQUEST_ENABLE_BLUETOOTH = 10; + +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/fragment/BaseEventFragment.java b/repeater-android/src/main/java/org/briarproject/repeater/fragment/BaseEventFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..4a7ce64a427510c6aa8fa4205dc0268fb932f830 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/fragment/BaseEventFragment.java @@ -0,0 +1,25 @@ +package org.briarproject.repeater.fragment; + +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; + +import javax.inject.Inject; + +public abstract class BaseEventFragment extends BaseFragment implements + EventListener { + + @Inject + protected volatile EventBus eventBus; + + @Override + public void onStart() { + super.onStart(); + eventBus.addListener(this); + } + + @Override + public void onStop() { + super.onStop(); + eventBus.removeListener(this); + } +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/fragment/ErrorFragment.java b/repeater-android/src/main/java/org/briarproject/repeater/fragment/ErrorFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..4b1c3eaa3f9b5d7baf07b8f8d5855610f5221421 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/fragment/ErrorFragment.java @@ -0,0 +1,65 @@ +package org.briarproject.repeater.fragment; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.repeater.R; +import org.briarproject.repeater.activity.ActivityComponent; + + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ErrorFragment extends BaseFragment { + + private static final String TAG = ErrorFragment.class.getSimpleName(); + + private static final String ERROR_MSG = "errorMessage"; + + public static ErrorFragment newInstance(String message) { + ErrorFragment f = new ErrorFragment(); + Bundle args = new Bundle(); + args.putString(ERROR_MSG, message); + f.setArguments(args); + return f; + } + + private String errorMessage; + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + if (args == null) throw new AssertionError(); + errorMessage = args.getString(ERROR_MSG); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater + .inflate(R.layout.fragment_error, container, false); + TextView msg = v.findViewById(R.id.errorMessage); + msg.setText(errorMessage); + return v; + } + + @Override + public void injectFragment(ActivityComponent component) { + // not necessary + } + +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/CameraException.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/CameraException.java new file mode 100644 index 0000000000000000000000000000000000000000..61520bfb811fae63bf8f1a197fd32e5bc81e3647 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/CameraException.java @@ -0,0 +1,14 @@ +package org.briarproject.repeater.keyagreement; + +import java.io.IOException; + +class CameraException extends IOException { + + CameraException(String message) { + super(message); + } + + CameraException(Throwable cause) { + super(cause); + } +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/CameraView.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/CameraView.java new file mode 100644 index 0000000000000000000000000000000000000000..117de7748aed71218761504a8af91feacd18a8f5 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/CameraView.java @@ -0,0 +1,524 @@ +package org.briarproject.repeater.keyagreement; + +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.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; + +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; +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_AUTO; +import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE; +import static android.os.Build.VERSION.SDK_INT; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.logException; + +@SuppressWarnings("deprecation") +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class CameraView extends SurfaceView implements SurfaceHolder.Callback, + AutoFocusCallback, View.OnClickListener { + + // Heuristic for the ideal preview size - small previews don't have enough + // detail, large previews are slow to decode + private static final int IDEAL_PIXELS = 500 * 1000; + + private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds + + private static final Logger LOG = + Logger.getLogger(CameraView.class.getName()); + + private final Runnable autoFocusRetry = this::retryAutoFocus; + + @Nullable + private Camera camera = null; + private int cameraIndex = 0; + private PreviewConsumer previewConsumer = null; + private Surface surface = null; + private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0; + private boolean previewStarted = false; + private boolean autoFocusSupported = false, autoFocusRunning = false; + + public CameraView(Context context) { + super(context); + } + + public CameraView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @UiThread + public void setPreviewConsumer(PreviewConsumer previewConsumer) { + LOG.info("Setting preview consumer"); + this.previewConsumer = previewConsumer; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + setKeepScreenOn(true); + getHolder().addCallback(this); + setOnClickListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + setKeepScreenOn(false); + getHolder().removeCallback(this); + } + + @UiThread + public void start() throws CameraException { + LOG.info("Opening camera"); + try { + int cameras = Camera.getNumberOfCameras(); + if (cameras == 0) throw new CameraException("No camera"); + // Try to find a back-facing camera + for (int i = 0; i < cameras; i++) { + CameraInfo info = new CameraInfo(); + Camera.getCameraInfo(i, info); + if (info.facing == CAMERA_FACING_BACK) { + LOG.info("Using back-facing camera"); + camera = Camera.open(i); + cameraIndex = i; + break; + } + } + // If we can't find a back-facing camera, use a front-facing one + if (camera == null) { + LOG.info("Using front-facing camera"); + camera = Camera.open(0); + cameraIndex = 0; + } + } catch (RuntimeException e) { + throw new CameraException(e); + } + setDisplayOrientation(getScreenRotationDegrees()); + // Use barcode scene mode if it's available + Parameters params = camera.getParameters(); + params = setSceneMode(camera, params); + if (SCENE_MODE_BARCODE.equals(params.getSceneMode())) { + // If the scene mode enabled the flash, try to disable it + if (!FLASH_MODE_OFF.equals(params.getFlashMode())) + params = disableFlash(camera, params); + // If the flash is still enabled, disable the scene mode + if (!FLASH_MODE_OFF.equals(params.getFlashMode())) + params = disableSceneMode(camera, params); + } + // Use the best available focus mode, preview size and other options + params = setBestParameters(camera, params); + // Enable auto focus if the selected focus mode uses it + enableAutoFocus(params.getFocusMode()); + // Log the parameters that are being used (maybe not what we asked for) + logCameraParameters(); + // Start the preview when the camera and the surface are both ready + if (surface != null && !previewStarted) startPreview(getHolder()); + } + + @UiThread + public void stop() throws CameraException { + if (camera == null) return; + stopPreview(); + LOG.info("Releasing camera"); + try { + camera.release(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + 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"); + if (camera == null) throw new CameraException("Camera is null"); + try { + camera.setPreviewDisplay(holder); + camera.startPreview(); + previewStarted = true; + startConsumer(); + } catch (IOException | RuntimeException e) { + throw new CameraException(e); + } + } + + @UiThread + private void stopPreview() throws CameraException { + LOG.info("Stopping preview"); + if (camera == null) throw new CameraException("Camera is null"); + try { + stopConsumer(); + camera.stopPreview(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + previewStarted = false; + } + + @UiThread + private void startConsumer() throws CameraException { + if (camera == null) throw new CameraException("Camera is null"); + startAutoFocus(); + previewConsumer.start(camera, cameraIndex); + } + + @UiThread + private void startAutoFocus() throws CameraException { + if (camera != null && autoFocusSupported && !autoFocusRunning) { + try { + removeCallbacks(autoFocusRetry); + camera.autoFocus(this); + autoFocusRunning = true; + } catch (RuntimeException e) { + throw new CameraException(e); + } + } + } + + @UiThread + private void stopConsumer() throws CameraException { + if (camera == null) throw new CameraException("Camera is null"); + cancelAutoFocus(); + previewConsumer.stop(); + } + + @UiThread + private void cancelAutoFocus() throws CameraException { + if (camera != null && autoFocusSupported && autoFocusRunning) { + try { + removeCallbacks(autoFocusRetry); + camera.cancelAutoFocus(); + autoFocusRunning = false; + } catch (RuntimeException e) { + throw new CameraException(e); + } + } + } + + /** + * See {@link Camera#setDisplayOrientation(int)}. + */ + @UiThread + private void setDisplayOrientation(int rotationDegrees) + throws CameraException { + if (camera == null) throw new CameraException("Camera is null"); + int orientation; + CameraInfo info = new CameraInfo(); + try { + Camera.getCameraInfo(cameraIndex, info); + } catch (RuntimeException e) { + throw new CameraException(e); + } + 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("Screen rotation " + rotationDegrees + + " degrees, camera orientation " + orientation + + " degrees"); + } + try { + camera.setDisplayOrientation(orientation); + } catch (RuntimeException e) { + throw new CameraException(e); + } + displayOrientation = orientation; + } + + @UiThread + private Parameters setSceneMode(Camera camera, Parameters params) + throws CameraException { + List<String> sceneModes = params.getSupportedSceneModes(); + if (sceneModes == null) return params; + if (LOG.isLoggable(INFO)) LOG.info("Scene modes: " + sceneModes); + if (sceneModes.contains(SCENE_MODE_BARCODE)) { + params.setSceneMode(SCENE_MODE_BARCODE); + try { + camera.setParameters(params); + return camera.getParameters(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + } + return params; + } + + @UiThread + private Parameters disableFlash(Camera camera, Parameters params) + throws CameraException { + params.setFlashMode(FLASH_MODE_OFF); + try { + camera.setParameters(params); + return camera.getParameters(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + } + + @UiThread + private Parameters disableSceneMode(Camera camera, Parameters params) + throws CameraException { + params.setSceneMode(SCENE_MODE_AUTO); + try { + camera.setParameters(params); + return camera.getParameters(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + } + + @UiThread + private Parameters setBestParameters(Camera camera, Parameters params) + throws CameraException { + setVideoStabilisation(params); + setFocusMode(params); + params.setFlashMode(FLASH_MODE_OFF); + setPreviewSize(params); + try { + camera.setParameters(params); + return camera.getParameters(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + } + + @UiThread + private void setVideoStabilisation(Parameters params) { + if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) { + params.setVideoStabilization(true); + } + } + + @UiThread + private void setFocusMode(Parameters params) { + List<String> focusModes = params.getSupportedFocusModes(); + if (LOG.isLoggable(INFO)) LOG.info("Focus modes: " + focusModes); + if (focusModes.contains(FOCUS_MODE_CONTINUOUS_PICTURE)) { + params.setFocusMode(FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (focusModes.contains(FOCUS_MODE_CONTINUOUS_VIDEO)) { + params.setFocusMode(FOCUS_MODE_CONTINUOUS_VIDEO); + } else if (focusModes.contains(FOCUS_MODE_EDOF)) { + params.setFocusMode(FOCUS_MODE_EDOF); + } else if (focusModes.contains(FOCUS_MODE_MACRO)) { + params.setFocusMode(FOCUS_MODE_MACRO); + } else if (focusModes.contains(FOCUS_MODE_AUTO)) { + params.setFocusMode(FOCUS_MODE_AUTO); + } else if (focusModes.contains(FOCUS_MODE_FIXED)) { + params.setFocusMode(FOCUS_MODE_FIXED); + } + } + + @UiThread + private void setPreviewSize(Parameters params) { + if (surfaceWidth == 0 || surfaceHeight == 0) return; + // Choose a preview size that's close to the aspect ratio of the + // surface and close to the ideal size for decoding + float idealRatio = (float) surfaceWidth / surfaceHeight; + 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); + float pixels = width * height; + float zoom = Math.max(pixels / IDEAL_PIXELS, IDEAL_PIXELS / pixels); + float score = 1 / (stretch * zoom); + if (LOG.isLoggable(INFO)) { + LOG.info("Size " + size.width + "x" + size.height + + ", stretch " + stretch + ", zoom " + zoom + + ", score " + score); + } + 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); + } + } + + @UiThread + private void enableAutoFocus(String focusMode) { + autoFocusSupported = FOCUS_MODE_AUTO.equals(focusMode) || + FOCUS_MODE_MACRO.equals(focusMode); + } + + @UiThread + private void logCameraParameters() throws CameraException { + if (camera == null) throw new AssertionError(); + if (LOG.isLoggable(INFO)) { + Parameters params; + try { + params = camera.getParameters(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + if (SDK_INT >= 15) { + LOG.info("Video stabilisation enabled: " + + params.getVideoStabilization()); + } + LOG.info("Scene mode: " + params.getSceneMode()); + LOG.info("Focus mode: " + params.getFocusMode()); + LOG.info("Flash mode: " + params.getFlashMode()); + Size size = params.getPreviewSize(); + LOG.info("Preview size: " + size.width + "x" + size.height); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + post(() -> { + try { + surfaceCreatedUi(holder); + } catch (CameraException e) { + logException(LOG, WARNING, e); + } + }); + } + + @UiThread + private void surfaceCreatedUi(SurfaceHolder holder) throws CameraException { + LOG.info("Surface created"); + if (surface != null && surface != holder.getSurface()) { + LOG.info("Releasing old surface"); + surface.release(); + } + surface = holder.getSurface(); + // We'll start the preview when surfaceChanged() is called + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + post(() -> { + try { + surfaceChangedUi(holder, w, h); + } catch (CameraException e) { + logException(LOG, WARNING, e); + } + }); + } + + @UiThread + private void surfaceChangedUi(SurfaceHolder holder, int w, int h) + throws CameraException { + if (LOG.isLoggable(INFO)) LOG.info("Surface changed: " + w + "x" + h); + if (surface != null && surface != holder.getSurface()) { + LOG.info("Releasing old surface"); + surface.release(); + } + surface = holder.getSurface(); + surfaceWidth = w; + surfaceHeight = h; + if (camera == null) return; // We are stopped + if (previewStarted) stopPreview(); + try { + Parameters params = camera.getParameters(); + setPreviewSize(params); + camera.setParameters(params); + logCameraParameters(); + } catch (RuntimeException e) { + throw new CameraException(e); + } + startPreview(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + post(() -> surfaceDestroyedUi(holder)); + } + + @UiThread + private void surfaceDestroyedUi(SurfaceHolder holder) { + LOG.info("Surface destroyed"); + if (surface != null && surface != holder.getSurface()) { + LOG.info("Releasing old surface"); + surface.release(); + } + surface = null; + holder.getSurface().release(); + } + + @Override + public void onAutoFocus(boolean success, Camera camera) { + if (LOG.isLoggable(INFO)) + LOG.info("Auto focus succeeded: " + success); + autoFocusRunning = false; + postDelayed(autoFocusRetry, AUTO_FOCUS_RETRY_DELAY); + } + + @UiThread + private void retryAutoFocus() { + try { + startAutoFocus(); + } catch (CameraException e) { + logException(LOG, WARNING, e); + } + } + + @Override + public void onClick(View v) { + retryAutoFocus(); + } +} \ No newline at end of file diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/ContactExchangeActivity.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/ContactExchangeActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..31c6f7d1d6127022b2dbe80a6b41742bc95130cc --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/ContactExchangeActivity.java @@ -0,0 +1,141 @@ +package org.briarproject.repeater.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.repeater.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; +import static org.briarproject.briar.repeater.R.*; + +@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) { + 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, string.connection_failed, + LENGTH_LONG).show(); + } + + @UiThread + @Override + public String keyAgreementWaiting() { + return getString(string.waiting_for_contact_to_scan); + } + + @UiThread + @Override + public String keyAgreementStarted() { + return getString(string.authenticating_with_device); + } + + @UiThread + @Override + public String keyAgreementAborted(boolean remoteAborted) { + // TODO show abort somewhere persistent? + Toast.makeText(this, + remoteAborted ? string.connection_aborted_remote : + 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/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/IntroFragment.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/IntroFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..5a19a86bb7f222813e8589f7b59b1916af53ddda --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/IntroFragment.java @@ -0,0 +1,79 @@ +package org.briarproject.repeater.keyagreement; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.repeater.R; +import org.briarproject.repeater.activity.ActivityComponent; + +import javax.annotation.Nullable; + +import org.briarproject.repeater.fragment.BaseFragment; + +import static android.view.View.FOCUS_DOWN; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class IntroFragment extends BaseFragment { + + interface IntroScreenSeenListener { + void showNextScreen(); + } + + public static final String TAG = IntroFragment.class.getName(); + + private IntroScreenSeenListener screenSeenListener; + private ScrollView scrollView; + + public static IntroFragment newInstance() { + + Bundle args = new Bundle(); + + IntroFragment fragment = new IntroFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + screenSeenListener = (IntroScreenSeenListener) context; + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + + View v = inflater.inflate(R.layout.fragment_keyagreement_id, container, + false); + scrollView = v.findViewById(R.id.scrollView); + View button = v.findViewById(R.id.continueButton); + button.setOnClickListener(view -> screenSeenListener.showNextScreen()); + return v; + } + + @Override + public void onStart() { + super.onStart(); + scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); + } + +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/KeyAgreementActivity.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/KeyAgreementActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..c59c85ba64b6b54e1d500a6cd3cf0f58c7e17895 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/KeyAgreementActivity.java @@ -0,0 +1,257 @@ +package org.briarproject.repeater.keyagreement; + +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.annotation.UiThread; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.FragmentManager; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog.Builder; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.widget.Toast; + +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent; +import org.briarproject.repeater.activity.ActivityComponent; + +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import org.briarproject.repeater.activity.BriarActivity; +import org.briarproject.repeater.fragment.BaseFragment; + +import static org.briarproject.repeater.activity.RequestCodes.REQUEST_ENABLE_BLUETOOTH; +import static org.briarproject.repeater.activity.RequestCodes.REQUEST_PERMISSION_CAMERA; +import static android.Manifest.permission.CAMERA; +import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE; +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.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION.SDK_INT; +import static android.widget.Toast.LENGTH_LONG; +import static org.briarproject.briar.repeater.R.*; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public abstract class KeyAgreementActivity extends BriarActivity implements + BaseFragment.BaseFragmentListener, IntroFragment.IntroScreenSeenListener, + KeyAgreementFragment.KeyAgreementEventListener { + + private enum BluetoothState { + UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED + } + + private static final Logger LOG = + Logger.getLogger(KeyAgreementActivity.class.getName()); + + @Inject + EventBus eventBus; + + private boolean isResumed = false, enableWasRequested = false; + private boolean continueClicked, gotCameraPermission; + private BluetoothState bluetoothState = BluetoothState.UNKNOWN; + private BroadcastReceiver bluetoothReceiver = null; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @SuppressWarnings("ConstantConditions") + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + setContentView(layout.activity_fragment_container_toolbar); + Toolbar toolbar = findViewById(id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + if (state == null) { + showInitialFragment(IntroFragment.newInstance()); + } + IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED); + bluetoothReceiver = new BluetoothStateReceiver(); + registerReceiver(bluetoothReceiver, filter); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (bluetoothReceiver != null) unregisterReceiver(bluetoothReceiver); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onPostResume() { + super.onPostResume(); + isResumed = true; + // Workaround for + // https://code.google.com/p/android/issues/detail?id=190966 + if (canShowQrCodeFragment()) showQrCodeFragment(); + } + + private boolean canShowQrCodeFragment() { + return isResumed && continueClicked + && (SDK_INT < 23 || gotCameraPermission) + && bluetoothState != BluetoothState.UNKNOWN + && bluetoothState != BluetoothState.WAITING; + } + + @Override + protected void onPause() { + super.onPause(); + isResumed = false; + } + + @Override + public void showNextScreen() { + continueClicked = true; + if (checkPermissions()) { + if (shouldRequestEnableBluetooth()) requestEnableBluetooth(); + else if (canShowQrCodeFragment()) showQrCodeFragment(); + } + } + + private boolean shouldRequestEnableBluetooth() { + return bluetoothState == BluetoothState.UNKNOWN + || bluetoothState == BluetoothState.REFUSED; + } + + private void requestEnableBluetooth() { + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt == null) { + setBluetoothState(BluetoothState.NO_ADAPTER); + } else if (bt.isEnabled()) { + setBluetoothState(BluetoothState.ENABLED); + } else { + enableWasRequested = true; + setBluetoothState(BluetoothState.WAITING); + Intent i = new Intent(ACTION_REQUEST_ENABLE); + startActivityForResult(i, REQUEST_ENABLE_BLUETOOTH); + } + } + + private void setBluetoothState(BluetoothState bluetoothState) { + LOG.info("Setting Bluetooth state to " + bluetoothState); + this.bluetoothState = bluetoothState; + if (enableWasRequested && bluetoothState == BluetoothState.ENABLED) { + eventBus.broadcast(new BluetoothEnabledEvent()); + enableWasRequested = false; + } + if (canShowQrCodeFragment()) showQrCodeFragment(); + } + + @Override + public void onActivityResult(int request, int result, Intent data) { + // If the request was granted we'll catch the state change event + if (request == REQUEST_ENABLE_BLUETOOTH && result == RESULT_CANCELED) + setBluetoothState(BluetoothState.REFUSED); + } + + private void showQrCodeFragment() { + continueClicked = false; + // FIXME #824 + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) { + BaseFragment f = KeyAgreementFragment.newInstance(this); + fm.beginTransaction() + .replace(id.fragmentContainer, f, f.getUniqueTag()) + .addToBackStack(f.getUniqueTag()) + .commit(); + } + } + + private boolean checkPermissions() { + if (ContextCompat.checkSelfPermission(this, CAMERA) != + PERMISSION_GRANTED) { + // Should we show an explanation? + if (ActivityCompat.shouldShowRequestPermissionRationale(this, + CAMERA)) { + OnClickListener continueListener = + (dialog, which) -> requestPermission(); + Builder builder = new Builder(this, style.BriarDialogTheme); + builder.setTitle(string.permission_camera_title); + builder.setMessage(string.permission_camera_request_body); + builder.setNeutralButton(string.continue_button, + continueListener); + builder.show(); + } else { + requestPermission(); + } + gotCameraPermission = false; + return false; + } else { + gotCameraPermission = true; + return true; + } + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, new String[] {CAMERA}, + REQUEST_PERMISSION_CAMERA); + } + + @Override + @UiThread + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + if (requestCode == REQUEST_PERMISSION_CAMERA) { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && + grantResults[0] == PERMISSION_GRANTED) { + gotCameraPermission = true; + showNextScreen(); + } else { + if (!ActivityCompat.shouldShowRequestPermissionRationale(this, + CAMERA)) { + /* + // The user has permanently denied the request + OnClickListener cancelListener = + (dialog, which) -> supportFinishAfterTransition(); + Builder builder = new Builder(this, style.BriarDialogTheme); + builder.setTitle(string.permission_camera_title); + builder.setMessage(string.permission_camera_denied_body); + builder.setPositiveButton(string.ok, + UiUtils.getGoToSettingsListener(this)); + builder.setNegativeButton(string.cancel, cancelListener); + builder.show(); + */ + } else { + Toast.makeText(this, string.permission_camera_denied_toast, + LENGTH_LONG).show(); + supportFinishAfterTransition(); + } + } + } + } + + private class BluetoothStateReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + int state = intent.getIntExtra(EXTRA_STATE, 0); + if (state == STATE_ON) setBluetoothState(BluetoothState.ENABLED); + else setBluetoothState(BluetoothState.UNKNOWN); + } + } +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/KeyAgreementFragment.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/KeyAgreementFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..2b26b63dd0b02d39d5b206eee0601c055053de8b --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/KeyAgreementFragment.java @@ -0,0 +1,372 @@ +package org.briarproject.repeater.keyagreement; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.support.annotation.UiThread; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.TextView; +import android.widget.Toast; + +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; +import org.briarproject.bramble.api.keyagreement.PayloadParser; +import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent; +import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent; +import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent; +import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent; +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.repeater.R; +import org.briarproject.repeater.activity.ActivityComponent; +import org.briarproject.repeater.view.QrCodeView; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Provider; + +import org.briarproject.repeater.fragment.BaseEventFragment; +import org.briarproject.repeater.fragment.ErrorFragment; + +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.logException; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class KeyAgreementFragment extends BaseEventFragment + implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener { + + 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"); + + @Inject + Provider<KeyAgreementTask> keyAgreementTaskProvider; + @Inject + PayloadEncoder payloadEncoder; + @Inject + PayloadParser payloadParser; + @Inject + @IoExecutor + Executor ioExecutor; + @Inject + EventBus eventBus; + + private CameraView cameraView; + private LinearLayout cameraOverlay; + private QrCodeView qrCodeView; + private View statusView; + private TextView status; + + private boolean gotRemotePayload; + private volatile boolean gotLocalPayload; + private KeyAgreementTask task; + private KeyAgreementEventListener listener; + + public static KeyAgreementFragment newInstance( + KeyAgreementEventListener listener) { + Bundle args = new Bundle(); + KeyAgreementFragment fragment = new KeyAgreementFragment(); + fragment.listener = listener; + fragment.setArguments(args); + return fragment; + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_keyagreement_qr, container, + false); + } + + @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); + 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)); + } + + @Override + public void onStart() { + super.onStart(); + try { + cameraView.start(); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + } + startListening(); + } + + @Override + public void setFullscreen(boolean fullscreen) { + 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 + public void onStop() { + super.onStop(); + stopListening(); + try { + cameraView.stop(); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + } + } + + @UiThread + private void logCameraExceptionAndFinish(CameraException e) { + logException(LOG, WARNING, e); + Toast.makeText(getActivity(), R.string.camera_error, + LENGTH_LONG).show(); + finish(); + } + + @UiThread + private void startListening() { + KeyAgreementTask oldTask = task; + KeyAgreementTask newTask = keyAgreementTaskProvider.get(); + task = newTask; + ioExecutor.execute(() -> { + if (oldTask != null) oldTask.stopListening(); + newTask.listen(); + }); + } + + @UiThread + private void stopListening() { + KeyAgreementTask oldTask = task; + ioExecutor.execute(() -> { + if (oldTask != null) oldTask.stopListening(); + }); + } + + @UiThread + private void reset() { + // If we've stopped the camera view, restart it + if (gotRemotePayload) { + try { + cameraView.start(); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + return; + } + } + statusView.setVisibility(INVISIBLE); + cameraView.setVisibility(VISIBLE); + gotRemotePayload = false; + gotLocalPayload = false; + startListening(); + } + + @UiThread + private void qrCodeScanned(String content) { + try { + byte[] payloadBytes = content.getBytes(ISO_8859_1); + if (LOG.isLoggable(INFO)) + LOG.info("Remote payload is " + payloadBytes.length + " bytes"); + Payload remotePayload = payloadParser.parse(payloadBytes); + gotRemotePayload = true; + cameraView.stop(); + cameraView.setVisibility(INVISIBLE); + statusView.setVisibility(VISIBLE); + status.setText(R.string.connecting_to_device); + task.connectAndRunProtocol(remotePayload); + } catch (UnsupportedVersionException e) { + reset(); + String msg = getString(R.string.qr_code_unsupported, + getString(R.string.app_name)); + showNextFragment(ErrorFragment.newInstance(msg)); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); + } catch (IOException | IllegalArgumentException e) { + LOG.log(WARNING, "QR Code Invalid", e); + reset(); + 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; + gotLocalPayload = true; + 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) { + keyAgreementFinished(((KeyAgreementFinishedEvent) e).getResult()); + } + } + + private void keyAgreementFailed() { + runOnUiThreadUnlessDestroyed(() -> { + reset(); + listener.keyAgreementFailed(); + }); + } + + private void keyAgreementWaiting() { + runOnUiThreadUnlessDestroyed( + () -> status.setText(listener.keyAgreementWaiting())); + } + + private void keyAgreementStarted() { + runOnUiThreadUnlessDestroyed(() -> { + qrCodeView.setVisibility(INVISIBLE); + statusView.setVisibility(VISIBLE); + status.setText(listener.keyAgreementStarted()); + }); + } + + private void keyAgreementAborted(boolean remoteAborted) { + runOnUiThreadUnlessDestroyed(() -> { + reset(); + qrCodeView.setVisibility(VISIBLE); + statusView.setVisibility(INVISIBLE); + 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)); + }); + } + + @Override + public void handleResult(Result result) { + runOnUiThreadUnlessDestroyed(() -> { + LOG.info("Got result from decoder"); + // Ignore results until the KeyAgreementTask is ready + if (!gotLocalPayload) return; + if (!gotRemotePayload) qrCodeScanned(result.getText()); + }); + } + + @Override + 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/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/PreviewConsumer.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/PreviewConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..41ad494f10fe4caa52f44076e25d2f0e6e706063 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/PreviewConsumer.java @@ -0,0 +1,17 @@ +package org.briarproject.repeater.keyagreement; + +import android.hardware.Camera; +import android.support.annotation.UiThread; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@SuppressWarnings("deprecation") +@NotNullByDefault +interface PreviewConsumer { + + @UiThread + void start(Camera camera, int cameraIndex); + + @UiThread + void stop(); +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/QrCodeDecoder.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/QrCodeDecoder.java new file mode 100644 index 0000000000000000000000000000000000000000..05b6cdb57bedf7318e3676bc946603c27a2c0a84 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/QrCodeDecoder.java @@ -0,0 +1,148 @@ +package org.briarproject.repeater.keyagreement; + +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.Size; +import android.os.AsyncTask; +import android.support.annotation.UiThread; + +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 org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; + +import java.util.logging.Logger; + +import static com.google.zxing.DecodeHintType.CHARACTER_SET; +import static java.util.Collections.singletonMap; +import static java.util.logging.Level.WARNING; + +@SuppressWarnings("deprecation") +@MethodsNotNullByDefault +@ParametersNotNullByDefault +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 Camera camera = null; + private int cameraIndex = 0; + + QrCodeDecoder(ResultCallback callback) { + this.callback = callback; + } + + @Override + public void start(Camera camera, int cameraIndex) { + this.camera = camera; + this.cameraIndex = cameraIndex; + askForPreviewFrame(); + } + + @Override + public void stop() { + camera = null; + cameraIndex = 0; + } + + @UiThread + private void askForPreviewFrame() { + if (camera != null) camera.setOneShotPreviewCallback(this); + } + + @UiThread + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if (camera == this.camera) { + try { + Size size = camera.getParameters().getPreviewSize(); + // The preview should be in NV21 format: width * height bytes of + // Y followed by width * height / 2 bytes of interleaved U and V + if (data.length == size.width * size.height * 3 / 2) { + CameraInfo info = new CameraInfo(); + Camera.getCameraInfo(cameraIndex, info); + new DecoderTask(data, size.width, size.height, + info.orientation).execute(); + } else { + // Camera parameters have changed - ask for a new preview + LOG.info("Preview size does not match camera parameters"); + askForPreviewFrame(); + } + } catch (RuntimeException e) { + LOG.log(WARNING, "Error getting camera parameters.", e); + } + } else { + LOG.info("Camera has changed, ignoring preview frame"); + } + } + + private class DecoderTask extends AsyncTask<Void, Void, Void> { + + private final byte[] data; + private final int width, height, orientation; + + private DecoderTask(byte[] data, int width, int height, + int orientation) { + this.data = data; + this.width = width; + this.height = height; + this.orientation = orientation; + } + + @Override + protected Void doInBackground(Void... params) { + BinaryBitmap bitmap = binarize(data, width, height, orientation); + Result result; + try { + result = reader.decode(bitmap, + singletonMap(CHARACTER_SET, "ISO8859_1")); + } catch (ReaderException e) { + // No barcode found + return null; + } catch (RuntimeException e) { + LOG.warning("Invalid preview frame"); + return null; + } finally { + reader.reset(); + } + callback.handleResult(result); + return null; + } + + @Override + protected void onPostExecute(Void result) { + askForPreviewFrame(); + } + } + + private static BinaryBitmap binarize(byte[] data, int width, int height, + int orientation) { + // Crop to a square at the top (portrait) or left (landscape) of the + // screen - this will be faster to decode and should include + // everything visible in the viewfinder + int crop = Math.min(width, height); + int left = orientation >= 180 ? width - crop : 0; + int top = orientation >= 180 ? height - crop : 0; + LuminanceSource src = new PlanarYUVLuminanceSource(data, width, + height, left, top, crop, crop, false); + return new BinaryBitmap(new HybridBinarizer(src)); + } + + @NotNullByDefault + interface ResultCallback { + + void handleResult(Result result); + } +} diff --git a/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/QrCodeUtils.java b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/QrCodeUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..9d8efbe6d9c1518c64ee9e57330eb8fb3416f6e1 --- /dev/null +++ b/repeater-android/src/main/java/org/briarproject/repeater/keyagreement/QrCodeUtils.java @@ -0,0 +1,56 @@ +package org.briarproject.repeater.keyagreement; + +import android.graphics.Bitmap; +import android.util.DisplayMetrics; + +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static android.graphics.Bitmap.Config.ARGB_8888; +import static android.graphics.Color.BLACK; +import static android.graphics.Color.WHITE; +import static com.google.zxing.BarcodeFormat.QR_CODE; +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +class QrCodeUtils { + + private static final Logger LOG = + Logger.getLogger(QrCodeUtils.class.getName()); + + @Nullable + static Bitmap createQrCode(DisplayMetrics dm, String input) { + int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels); + try { + // Generate QR code + BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE, + smallestDimen, smallestDimen); + return renderQrCode(encoded); + } catch (WriterException e) { + logException(LOG, WARNING, e); + return null; + } + } + + private static Bitmap renderQrCode(BitMatrix matrix) { + int width = matrix.getWidth(); + int height = matrix.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] = matrix.get(x, y) ? BLACK : WHITE; + } + } + Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888); + qr.setPixels(pixels, 0, width, 0, 0, width, height); + return qr; + } +} diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/ActivityComponent.java b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/ActivityComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..8261c9517037e8d4a33bb9230bba90a1bbe7f5de --- /dev/null +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/ActivityComponent.java @@ -0,0 +1,26 @@ +package org.briarproject.repeater.activity; + +import android.app.Activity; + +import org.briarproject.repeater.AndroidComponent; +import org.briarproject.repeater.keyagreement.ContactExchangeActivity; +import org.briarproject.repeater.keyagreement.IntroFragment; +import org.briarproject.repeater.keyagreement.KeyAgreementActivity; +import org.briarproject.repeater.keyagreement.KeyAgreementFragment; + +import dagger.Component; + +@ActivityScope +@Component( + dependencies = AndroidComponent.class) +public interface ActivityComponent { + + void inject(NavDrawerActivity activity); + void inject(ContactExchangeActivity activity); + void inject(KeyAgreementActivity activity); + + // Fragments + void inject(IntroFragment fragment); + void inject(KeyAgreementFragment fragment); + +} diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/ActivityScope.java b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/ActivityScope.java new file mode 100644 index 0000000000000000000000000000000000000000..c5429cb433bb305e93f08d382260005d6f02b8b8 --- /dev/null +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/ActivityScope.java @@ -0,0 +1,11 @@ +package org.briarproject.repeater.activity; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +@Scope +@Retention(RetentionPolicy.RUNTIME) +public @interface ActivityScope { +} diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/BaseActivity.java b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/BaseActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..f61eeb118cc508516fde7bea43555dd5300f0bdd --- /dev/null +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/BaseActivity.java @@ -0,0 +1,99 @@ +package org.briarproject.repeater.activity; + +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.UiThread; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.briar.repeater.R; +import org.briarproject.repeater.AndroidComponent; +import org.briarproject.repeater.RepeaterApplication; + +import javax.annotation.Nullable; + +import org.briarproject.repeater.fragment.BaseFragment; + +import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; + +public abstract class BaseActivity extends AppCompatActivity +{ + private ActivityComponent activityComponent; + + private boolean destroyed = false; + + @Nullable + private Toolbar toolbar = null; + private boolean searchedForToolbar = false; + + public abstract void injectActivity(ActivityComponent component); + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + + AndroidComponent applicationComponent = + ((RepeaterApplication) getApplication()).getApplicationComponent(); + + activityComponent = DaggerActivityComponent.builder() + .androidComponent(applicationComponent) + .build(); + + injectActivity(activityComponent); + + } + + public ActivityComponent getActivityComponent() { + return activityComponent; + } + + + protected void showInitialFragment(BaseFragment f) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragmentContainer, f, f.getUniqueTag()) + .commit(); + } + + public void showNextFragment(BaseFragment f) { + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(R.anim.step_next_in, + R.anim.step_previous_out, R.anim.step_previous_in, + R.anim.step_next_out) + .replace(R.id.fragmentContainer, f, f.getUniqueTag()) + .addToBackStack(f.getUniqueTag()) + .commit(); + } + + + @Override + protected void onDestroy() { + super.onDestroy(); + destroyed = true; + } + + public void runOnUiThreadUnlessDestroyed(Runnable r) { + runOnUiThread(() -> { + if (!destroyed && !isFinishing()) r.run(); + }); + } + + public void showSoftKeyboard(View view) { + Object o = getSystemService(INPUT_METHOD_SERVICE); + ((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT); + } + + public void hideSoftKeyboard(View view) { + IBinder token = view.getWindowToken(); + Object o = getSystemService(INPUT_METHOD_SERVICE); + ((InputMethodManager) o).hideSoftInputFromWindow(token, 0); + } + + @UiThread + public void handleDbException(DbException e) { + supportFinishAfterTransition(); + } +} diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/NavDrawerActivity.kt b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/NavDrawerActivity.kt index 6427d62109410b092c25b878930c02c23d7b3fb0..d2a0da4db8e2f9b63e03a8d2e3af2d2fe1b3acd1 100644 --- a/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/NavDrawerActivity.kt +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/NavDrawerActivity.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.support.design.widget.NavigationView import android.support.v4.view.GravityCompat.START import android.support.v7.app.ActionBarDrawerToggle -import android.support.v7.app.AppCompatActivity import android.view.Menu import android.view.MenuItem import kotlinx.android.synthetic.main.activity_nav_drawer.* @@ -18,8 +17,12 @@ import org.briarproject.repeater.RepeaterService import org.briarproject.repeater.fragment.BaseFragment import org.briarproject.repeater.fragment.OverviewFragment import org.briarproject.repeater.fragment.UserListFragment +import org.briarproject.repeater.keyagreement.ContactExchangeActivity -class NavDrawerActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { +class NavDrawerActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, BaseFragment.BaseFragmentListener { + + override fun injectActivity(component: ActivityComponent?) { + } private lateinit var drawerToggle: ActionBarDrawerToggle @@ -70,7 +73,8 @@ class NavDrawerActivity : AppCompatActivity(), NavigationView.OnNavigationItemSe override fun onOptionsItemSelected(item: MenuItem?): Boolean { return when (item?.itemId) { R.id.action_add_repeater -> { - // TODO: add repeater screen + val intent = Intent(this, ContactExchangeActivity::class.java) + startActivity(intent) true } else -> @@ -102,9 +106,9 @@ class NavDrawerActivity : AppCompatActivity(), NavigationView.OnNavigationItemSe R.animator.fade_out, R.animator.fade_in, R.animator.fade_out) .replace(R.id.fragmentContainer, fragment, - fragment.getUniqueTag()) + fragment.uniqueTag) if (isAddedToBackStack) { - trans.addToBackStack(fragment.getUniqueTag()) + trans.addToBackStack(fragment.uniqueTag) } trans.commit() } diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/SetupActivity.java b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/SetupActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..330624888ccdd9681507666e1ca2cf68e32dcb0a --- /dev/null +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/activity/SetupActivity.java @@ -0,0 +1,4 @@ +package org.briarproject.repeater.activity; + +public class SetupActivity { +} diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/BaseFragment.kt b/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/BaseFragment.kt index 1ee9ece00f5dd26c71e48c65ea57f91914ef35b3..5234d8fba9062dd786ecd3be681c57117ec4f790 100644 --- a/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/BaseFragment.kt +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/BaseFragment.kt @@ -1,8 +1,88 @@ package org.briarproject.repeater.fragment +import android.content.Context +import android.os.Bundle +import android.support.annotation.UiThread import android.support.v4.app.Fragment +import android.view.MenuItem + +import org.briarproject.bramble.api.db.DbException +import org.briarproject.repeater.activity.ActivityComponent abstract class BaseFragment : Fragment() { - abstract fun getUniqueTag(): String -} \ No newline at end of file + protected lateinit var listener: BaseFragmentListener + + abstract val uniqueTag: String + + abstract fun injectFragment(component: ActivityComponent) + + override fun onAttach(context: Context?) { + super.onAttach(context) + listener = context as BaseFragmentListener + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // allow for "up" button to act as back button + setHasOptionsMenu(true) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + injectFragment(listener.getActivityComponent()) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + return when (item!!.itemId) { + android.R.id.home -> { + listener.onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + @UiThread + protected open fun finish() { + if (!isDetached) + activity!!.supportFinishAfterTransition() + } + + interface BaseFragmentListener { + + @UiThread + fun getActivityComponent(): ActivityComponent + + @UiThread + fun onBackPressed() + + @UiThread + fun showNextFragment(f: BaseFragment) + + @UiThread + fun handleDbException(e: DbException) + } + + protected fun showNextFragment(f: BaseFragment) { + listener.showNextFragment(f) + } + + @UiThread + protected fun handleDbException(e: DbException) { + listener.handleDbException(e) + } + + fun runOnUiThreadUnlessDestroyed(r: Runnable) { + val activity = activity + activity?.runOnUiThread { + // Note that we don't have to check if the activity has + // been destroyed as the Fragment has not been detached yet + if (!isDetached && !activity.isFinishing) { + r.run() + } + } + } + +} diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/OverviewFragment.kt b/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/OverviewFragment.kt index 25a29448ab3a5894d5a6132907951b265236e05b..22510c6d45a41fcc6529bc28c034be68baccff16 100644 --- a/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/OverviewFragment.kt +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/OverviewFragment.kt @@ -5,13 +5,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import org.briarproject.briar.repeater.R +import org.briarproject.repeater.activity.ActivityComponent class OverviewFragment : BaseFragment() { - companion object { - const val TAG = "OverviewFragment" - } + override val uniqueTag: String = "OverviewFragment" - override fun getUniqueTag() = TAG + override fun injectFragment(component: ActivityComponent) { + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity?.title = getString(R.string.overview) diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/UserListFragment.kt b/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/UserListFragment.kt index 6000f5bc76e5045fb0c7b00ebeea76bd704b42dc..21a483a8b7ee5daa5f6c131dbc18d0fc4d7fd0e3 100644 --- a/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/UserListFragment.kt +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/fragment/UserListFragment.kt @@ -7,17 +7,17 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import kotlinx.android.synthetic.main.fragment_userlist.* import org.briarproject.briar.repeater.R +import org.briarproject.repeater.activity.ActivityComponent class UserListFragment : BaseFragment() { - companion object { - const val TAG = "UserListFragment" + override val uniqueTag: String = "UserListFragment" + + override fun injectFragment(component: ActivityComponent) { } private val users: Array<String> = arrayOf() private lateinit var adapter: ArrayAdapter<String> - override fun getUniqueTag() = TAG - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity?.title = getString(R.string.users) return inflater.inflate(R.layout.fragment_userlist, container, false) diff --git a/repeater-android/src/main/kotlin/org/briarproject/repeater/view/QrCodeView.kt b/repeater-android/src/main/kotlin/org/briarproject/repeater/view/QrCodeView.kt new file mode 100644 index 0000000000000000000000000000000000000000..0238d44c92a2f5177f478024c92f5e068897eddc --- /dev/null +++ b/repeater-android/src/main/kotlin/org/briarproject/repeater/view/QrCodeView.kt @@ -0,0 +1,59 @@ +package org.briarproject.repeater.view + +import android.content.Context +import android.graphics.Bitmap +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 kotlinx.android.synthetic.main.qr_code_view.view.* + +import org.briarproject.briar.repeater.R + + +class QrCodeView(context: Context, + attrs: AttributeSet?) : FrameLayout(context, attrs) { + + private var fullscreen = false + private var listener: FullscreenListener? = null + + init { + val inflater = context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + inflater.inflate(R.layout.qr_code_view, this, true) + val fullscreenButton = findViewById<ImageView>(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 + fun setQrCode(qrCode: Bitmap) { + qr_code.setImageBitmap(qrCode) + // Simple fade-in animation + val anim = AlphaAnimation(0.0f, 1.0f) + anim.duration = 200 + qr_code.startAnimation(anim) + } + + @UiThread + fun setFullscreenListener(listener: FullscreenListener) { + this.listener = listener + } + + interface FullscreenListener { + fun setFullscreen(fullscreen: Boolean) + } + +} diff --git a/repeater-android/src/main/res/drawable/alerts_and_states_error.xml b/repeater-android/src/main/res/drawable/alerts_and_states_error.xml new file mode 100644 index 0000000000000000000000000000000000000000..02056c85cec5acdaae01308058a264b1c07b4a3f --- /dev/null +++ b/repeater-android/src/main/res/drawable/alerts_and_states_error.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="128dp" + android:height="128dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/> +</vector> diff --git a/repeater-android/src/main/res/drawable/border_explanation.xml b/repeater-android/src/main/res/drawable/border_explanation.xml new file mode 100644 index 0000000000000000000000000000000000000000..d3990cfc614884fbb32356a741afe9fa4d951052 --- /dev/null +++ b/repeater-android/src/main/res/drawable/border_explanation.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + + <solid + android:color="@android:color/transparent"/> + + <stroke + android:width="2dp" + android:color="@color/colorPrimary"/> + +</shape> \ No newline at end of file diff --git a/repeater-android/src/main/res/drawable/ic_fullscreen_black_48dp.xml b/repeater-android/src/main/res/drawable/ic_fullscreen_black_48dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..29b26803eb0d29731863f9c99917b0bee8eeba65 --- /dev/null +++ b/repeater-android/src/main/res/drawable/ic_fullscreen_black_48dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/> +</vector> diff --git a/repeater-android/src/main/res/drawable/ic_fullscreen_exit_black_48dp.xml b/repeater-android/src/main/res/drawable/ic_fullscreen_exit_black_48dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b62d104c1dba38e6b6b60aba19014442437b9ed --- /dev/null +++ b/repeater-android/src/main/res/drawable/ic_fullscreen_exit_black_48dp.xml @@ -0,0 +1,4 @@ +<vector android:height="48dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/> +</vector> diff --git a/repeater-android/src/main/res/drawable/qr_code_explanation.xml b/repeater-android/src/main/res/drawable/qr_code_explanation.xml new file mode 100644 index 0000000000000000000000000000000000000000..157025c222f4979db4f70804cf72b8e199a4e17c --- /dev/null +++ b/repeater-android/src/main/res/drawable/qr_code_explanation.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="400dp" + android:height="100dp" + android:viewportHeight="49.5" + android:viewportWidth="194.8"> + <path + android:fillColor="#000000" + android:pathData="M30.1 16.5l-9 0 0 -5c0 -2.4 -2 -4.4 -4.4 -4.4L4.4 7.1C2 7.1 0 9.1 0 11.5l0 24.2c0 2.4 2 4.4 4.4 4.4l9 0 0 5c0 2.4 2 4.4 4.4 4.4l12.2 0c2.4 0 4.4 -2 4.4 -4.4l0 -24.2c0.1 -2.4 -1.9 -4.4 -4.3 -4.4zm-27.4 16.1l0 -20.9 15.8 0 0 20.9 -15.8 0zm10.7 4.6l-5.8 0 0 -1.5 5.8 0 0 1.5zm13.5 9.4l-5.8 0 0 -1.5 5.8 0 0 1.5zm5 -4.6l-15.8 0 0 -1.9 0.5 0c2.4 0 4.4 -2 4.4 -4.4l0 -14.6 10.8 0 0 20.9z"/> + <path + android:fillColor="#000000" + android:pathData="M101.2 16.5l-8.3 0 0 -4.4c0 -1.4 -1.2 -2.6 -2.6 -2.6l-3.9 0 -2.1 -2.5 -6.9 0 -2.2 2.5 -3.8 0c-1.4 0 -2.6 1.2 -2.6 2.6l0 13.3c0 1.4 1.2 2.6 2.6 2.6l13.1 0 0 17.2c0 2.4 2 4.4 4.4 4.4l12.2 0c2.4 0 4.4 -2 4.4 -4.4l0 -24.3c0.2 -2.4 -1.8 -4.4 -4.3 -4.4zm-26.4 2.4c0 -3.3 2.7 -6 6 -6 3.3 0 6 2.7 6 6 0 3.3 -2.7 6 -6 6 -3.3 0 -6 -2.7 -6 -6zm23.2 27.7l-5.8 0 0 -1.5 5.8 0 0 1.5zm5 -4.6l-15.8 0 0 -14.1 3.1 0c1.4 0 2.6 -1.2 2.6 -2.6l0 -4.2 10.1 0 0 20.9z"/> + <path + android:fillColor="#000000" + android:pathData="M84.600003 18.9a3.8 3.8 0 0 1 -3.8 3.8 3.8 3.8 0 0 1 -3.8 -3.8 3.8 3.8 0 0 1 3.8 -3.8 3.8 3.8 0 0 1 3.8 3.8z"/> + <path + android:fillColor="#000000" + android:pathData="M175.3 16.5l-9.8 0 0 -5.7c0 -1.4 -1.2 -2.6 -2.6 -2.6l-19.3 0c-1.4 0 -2.6 1.2 -2.6 2.6l0 14.4c0 1.4 1.2 2.6 2.6 2.6l15.1 0 0 17.3c0 2.4 2 4.4 4.4 4.4l12.2 0c2.4 0 4.4 -2 4.4 -4.4l0 -24.2c0.1 -2.4 -1.9 -4.4 -4.4 -4.4zm-12.4 -5.9l-9.6 6 -9.6 -6 19.2 0zm-19.4 14.8l0 -12.3 9.8 6.1 9.8 -6.1 0 12.3 -19.6 0zm28.6 21.2l-5.8 0 0 -1.5 5.8 0 0 1.5zm5 -4.6l-15.8 0 0 -14.2 1.6 0c1.4 0 2.6 -1.2 2.6 -2.6l0 -4.1 11.6 0 0 20.9z"/> + <path + android:fillColor="#ff0000" + android:pathData="M101.4 17.8l2 2 7.4 -7.3 7.3 7.3 2.1 -2 -7.4 -7.4 7.4 -7.3 -2.1 -2.1 -7.3 7.4 -7.4 -7.4 -2 2.1 7.3 7.3z"/> + <path + android:fillColor="#ff0000" + android:pathData="M176 17.8l2.1 2 7.3 -7.3 7.4 7.3 2 -2 -7.3 -7.4 7.3 -7.3 -2 -2.1 -7.4 7.4 -7.3 -7.4 -2.1 2.1 7.3 7.3z"/> + <path + android:fillColor="#08b124" + android:pathData="M35.8 18.8l0 0L52.5 2.1 50.5 0 35.6 14.8 28.5 7.7l-2.1 2.1 9.2 9.1z"/> +</vector> \ No newline at end of file diff --git a/repeater-android/src/main/res/drawable/qr_code_intro.xml b/repeater-android/src/main/res/drawable/qr_code_intro.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b09db32dd279901f6b64b19a03ecff6bf58beb6 --- /dev/null +++ b/repeater-android/src/main/res/drawable/qr_code_intro.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="409dp" + android:height="162dp" + android:viewportHeight="161.7" + android:viewportWidth="409.2"> + <path + android:fillColor="#FF000000" + android:pathData="M369.8,157.4l-4.3,-4.3l-7.1,-2.4c-3.9,-1.3 -8.7,-3 -10.7,-3.7l-3.7,-1.3l3.5,-0.2c8.2,-0.4 13,-4 14.3,-10.9c0.8,-4.1 1.1,-17.3 0.8,-33c-0.2,-8.1 -0.2,-15.4 0,-16.3c0.1,-0.9 0.5,-2.4 0.9,-3.4c1.2,-3.5 0.3,-11.9 -1.9,-17.6c-0.3,-0.9 -1.9,-4.2 -3.5,-7.4c-4.2,-8.2 -4.5,-8.9 -4.9,-10.5c-0.5,-1.8 -0.2,-5.4 0.5,-6.8c0.7,-1.3 2.2,-2.9 3.2,-3.5c1.3,-0.7 2.6,0.1 4.7,2.9c3.4,4.5 14,19.4 15.7,22.2c3.7,6 6,11.2 8,18.8c0.7,2.5 1.9,7 2.7,10.1c0.8,3.1 2.7,10.2 4.1,15.8l2.6,10.2l4.6,5.2c2.6,2.9 5.8,6.5 7.2,8c1.4,1.6 2.5,3 2.5,3.2c0,0.3 -34.5,29.3 -34.9,29.3C374.2,161.7 372.2,159.7 369.8,157.4zM275.9,141c-1.3,-0.6 -2.2,-1.4 -2.9,-2.3c-2.1,-2.7 -2,2.4 -1.9,-68.5l0.1,-64l0.7,-1.2c1,-1.9 2,-2.9 3.7,-3.9l1.6,-0.9l37.8,-0.1c42.5,-0.1 39.4,-0.2 42.1,2.2c0.9,0.8 1.8,2 2.2,2.9c0.7,1.6 0.7,1.6 0.8,14.2l0.1,12.6l-1.8,-0.1c-1.4,-0.1 -2.1,0 -3.2,0.5c-2,1 -3.9,2.9 -5.1,5.1l-1,2l0,-12.8l0,-12.8h-33.6h-33.6v51.3v51.3h33.6h33.6l0.1,-34.4c0.1,-33 0.1,-34.4 0.6,-32.9c0.3,0.8 1.8,4 3.4,7c5.5,10.6 5.4,9.9 5.4,47.2c0,27.6 -0.1,30 -1.7,33.1c-1.1,2.2 -2.7,3.7 -5.1,4.7l-1.7,0.7L314,141.8l-36.2,0.1L275.9,141L275.9,141zM318.3,135.9c2.9,-1.3 4.5,-3.7 4.4,-6.6c0,-4.1 -3.1,-7.2 -7.1,-7.2c-2.1,0 -3.6,0.6 -5.2,2.2c-2.2,2.2 -2.8,5.4 -1.3,8.3c0.7,1.4 2.5,3 4,3.5C314.6,136.6 317,136.6 318.3,135.9z"/> + <path + android:fillColor="#FF000000" + android:pathData="M39.4,157.4l4.3,-4.3l7.1,-2.4c3.9,-1.3 8.7,-3 10.7,-3.7l3.7,-1.3l-3.5,-0.2c-8.2,-0.4 -13,-4 -14.3,-10.9c-0.8,-4.1 -1.1,-17.3 -0.8,-33c0.2,-8.1 0.2,-15.4 0,-16.3c-0.1,-0.9 -0.5,-2.4 -0.9,-3.4c-1.2,-3.5 -0.3,-11.9 1.9,-17.6c0.3,-0.9 1.9,-4.2 3.5,-7.4c4.2,-8.2 4.5,-8.9 4.9,-10.5c0.5,-1.8 0.2,-5.4 -0.5,-6.8c-0.7,-1.3 -2.2,-2.9 -3.2,-3.5c-1.3,-0.7 -2.6,0.1 -4.7,2.9c-3.4,4.5 -14,19.4 -15.7,22.2c-3.7,6 -6,11.2 -8,18.8c-0.7,2.5 -1.9,7 -2.7,10.1c-0.8,3.1 -2.7,10.2 -4.1,15.8l-2.6,10.2l-4.6,5.2c-2.6,2.9 -5.8,6.5 -7.2,8s-2.5,3 -2.5,3.2c0,0.3 34.5,29.3 34.9,29.3C35,161.7 37.1,159.7 39.4,157.4zM133.3,141c1.3,-0.6 2.2,-1.4 2.9,-2.3c2.1,-2.7 2,2.4 1.9,-68.5l-0.1,-64l-0.7,-1.2c-1,-1.9 -2,-2.9 -3.7,-3.9l-1.6,-0.9l-37.8,-0.1c-42.5,-0.1 -39.4,-0.2 -42.1,2.2c-0.9,0.8 -1.8,2 -2.2,2.9c-0.7,1.6 -0.7,1.6 -0.8,14.2L49,32l1.8,-0.1c1.4,-0.1 2.1,0 3.2,0.5c2,1 3.9,2.9 5.1,5.1l1,2l0,-12.8l0,-12.8h33.6h33.6v51.3v51.3L93.8,116.5L60.2,116.5l-0.1,-34.4c-0.1,-33 -0.1,-34.4 -0.6,-32.9c-0.3,0.8 -1.8,4 -3.4,7c-5.5,10.6 -5.4,9.9 -5.4,47.2c0,27.6 0.1,30 1.7,33.1c1.1,2.2 2.7,3.7 5.1,4.7l1.7,0.7l36.2,0.1l36.2,0.1L133.3,141L133.3,141zM90.9,135.9c-2.9,-1.3 -4.5,-3.7 -4.4,-6.6c0,-4.1 3.1,-7.2 7.1,-7.2c2.1,0 3.6,0.6 5.2,2.2c2.2,2.2 2.8,5.4 1.3,8.3c-0.7,1.4 -2.5,3 -4,3.5C94.6,136.6 92.3,136.6 90.9,135.9z"/> + <path + android:fillColor="#FF000000" + android:pathData="M80.5,63h2.3v2.3h2.3v2.3L73.6,67.6v-2.3h2.3v-4.6h4.6L80.5,63L80.5,63zM110.5,83.8h2.3v-2.3h-2.3L110.5,83.8zM82.8,63h2.3v-2.3h-2.3L82.8,63zM115.1,83.8h2.3v-2.3h-2.3L115.1,83.8zM87.4,86.1L92,86.1L92,83.8h-4.6L87.4,86.1zM108.2,86.1L108.2,83.8h-2.3v2.3L108.2,86.1zM99,86.1h2.3v-4.6L99,81.5L99,86.1zM80.5,56.1v2.3h6.9v-2.3L80.5,56.1zM78.2,58.4v-2.3h-4.6v4.6h2.3v-2.3L78.2,58.4zM85.1,53.8L69,53.8v-16.1h16.1L85.1,53.8zM82.8,40L71.3,40v11.5h11.5L82.8,40zM73.6,81.5h6.9v-6.9h-6.9L73.6,81.5zM96.6,79.1v2.3L99,81.4v-2.3L96.6,79.1zM80.5,42.3h-6.9v6.9h6.9L80.5,42.3zM117.4,37.7L117.4,53.8L101.3,53.8v-16.1L117.4,37.7zM115.1,40L103.6,40v11.5h11.5L115.1,40zM69,69.9h16.1v16.1L69,86L69,69.9zM71.3,83.8h11.5v-11.5L71.3,72.3L71.3,83.8zM71.3,56.1L69,56.1v11.5h2.3L71.3,56.1zM101.3,67.6v2.3h2.3v-2.3L101.3,67.6zM94.3,76.9v-2.3L92,74.6v2.3h-4.6v4.6L92,81.5v2.3h2.3v-4.6h2.3v-2.3L94.3,76.9zM87.4,46.9L92,46.9v-2.3h-4.6L87.4,46.9zM105.9,65.3h4.6v2.3h2.3v-6.9h-2.3v-4.6h-2.3v6.9h-6.9v2.3h2.3v2.3h2.3L105.9,65.3zM108.2,72.2h-2.3v-2.3h-2.3v4.6h-6.9v2.3h4.6v4.6h2.3v2.3h2.3v-4.6h9.2v-2.3h-6.9L108.2,72.2zM108.2,72.2h2.3v-4.6h-2.3L108.2,72.2zM89.7,72.2v-2.3L92,69.9v-2.3h2.3v-2.3h2.3v-4.6h6.9v-4.6h-2.3v2.3L99,58.4v-9.2h-2.3v-4.6L99,44.6v-6.9h-2.3v4.6h-2.3v-4.6h-6.9v4.6h2.3v-2.3L92,40v4.6h2.3v6.9h2.3v2.3h-2.3v4.6L92,58.4L92,53.8h-2.3v-2.3h-2.3v4.6h2.3v2.3h-2.3v6.9h2.3v-4.6L92,60.7v4.6h-2.3v2.3h-2.3v6.9L92,74.5v-2.3L89.7,72.2zM115.1,74.5v-2.3h-4.6v2.3L115.1,74.5zM112.8,42.3h-6.9v6.9h6.9L112.8,42.3zM94.3,72.2L99,72.2v-2.3h-2.3v-2.3h-2.3L94.4,72.2zM99,67.6v-2.3h-2.3v2.3L99,67.6zM112.8,58.4h4.6v-2.3h-4.6L112.8,58.4zM115.1,76.9h2.3v-2.3h-2.3L115.1,76.9zM115.1,63h2.3v-2.3h-2.3L115.1,63zM94.3,51.5L92,51.5v2.3h2.3L94.3,51.5zM94.3,51.5"/> + <path + android:fillColor="#FF000000" + android:pathData="M303.5,63h2.3v2.3h2.3v2.3h-11.5v-2.3h2.3v-4.6h4.6L303.5,63L303.5,63zM333.5,83.8h2.3v-2.3h-2.3L333.5,83.8zM305.8,63h2.3v-2.3h-2.3L305.8,63zM338.1,83.8h2.3v-2.3h-2.3L338.1,83.8zM310.4,86.1h4.6L315,83.8h-4.6L310.4,86.1zM331.2,86.1L331.2,83.8h-2.3v2.3L331.2,86.1zM322,86.1h2.3v-4.6L322,81.5L322,86.1zM303.5,56.1v2.3h6.9v-2.3L303.5,56.1zM301.2,58.4v-2.3h-4.6v4.6h2.3v-2.3L301.2,58.4zM308.1,53.8L292,53.8v-16.1h16.1L308.1,53.8zM305.8,40h-11.5v11.5h11.5L305.8,40zM296.6,81.5h6.9v-6.9h-6.9L296.6,81.5zM319.6,79.1v2.3h2.3v-2.3L319.6,79.1zM303.5,42.3h-6.9v6.9h6.9L303.5,42.3zM340.4,37.7L340.4,53.8h-16.1v-16.1L340.4,37.7zM338.1,40h-11.5v11.5h11.5L338.1,40zM292,69.9h16.1v16.1L292,86L292,69.9zM294.3,83.8h11.5v-11.5h-11.5L294.3,83.8zM294.3,56.1L292,56.1v11.5h2.3L294.3,56.1zM324.3,67.6v2.3h2.3v-2.3L324.3,67.6zM317.3,76.9v-2.3L315,74.6v2.3h-4.6v4.6h4.6v2.3h2.3v-4.6h2.3v-2.3L317.3,76.9zM310.4,46.9h4.6v-2.3h-4.6L310.4,46.9zM328.9,65.3h4.6v2.3h2.3v-6.9h-2.3v-4.6h-2.3v6.9h-6.9v2.3h2.3v2.3h2.3L328.9,65.3zM331.2,72.2h-2.3v-2.3h-2.3v4.6h-6.9v2.3h4.6v4.6h2.3v2.3h2.3v-4.6h9.2v-2.3h-6.9L331.2,72.2zM331.2,72.2h2.3v-4.6h-2.3L331.2,72.2zM312.7,72.2v-2.3h2.3v-2.3h2.3v-2.3h2.3v-4.6h6.9v-4.6h-2.3v2.3L322,58.4v-9.2h-2.3v-4.6h2.3v-6.9h-2.3v4.6h-2.3v-4.6h-6.9v4.6h2.3v-2.3h2.3v4.6h2.3v6.9h2.3v2.3h-2.3v4.6L315,58.4L315,53.8h-2.3v-2.3h-2.3v4.6h2.3v2.3h-2.3v6.9h2.3v-4.6h2.3v4.6h-2.3v2.3h-2.3v6.9h4.6v-2.3L312.7,72.2zM338.1,74.5v-2.3h-4.6v2.3L338.1,74.5zM335.8,42.3h-6.9v6.9h6.9L335.8,42.3zM317.3,72.2h4.6v-2.3h-2.3v-2.3h-2.3L317.3,72.2zM322,67.6v-2.3h-2.3v2.3L322,67.6zM335.8,58.4h4.6v-2.3h-4.6L335.8,58.4zM338.1,76.9h2.3v-2.3h-2.3L338.1,76.9zM338.1,63h2.3v-2.3h-2.3L338.1,63zM317.3,51.5L315,51.5v2.3h2.3L317.3,51.5zM317.3,51.5"/> + <path + android:fillColor="#FF000000" + android:pathData="M179.6,48.9l-20.6,18l20.6,16.7v-5.2L199,78.4v-24.3h-19.3L179.7,48.9z"/> + <path + android:fillColor="#FF000000" + android:pathData="M229.4,83.7l20.6,-18l-20.6,-16.7v5.2L210,54.2v24.3h19.3L229.3,83.7z"/> +</vector> diff --git a/repeater-android/src/main/res/layout/activity_fragment_container_toolbar.xml b/repeater-android/src/main/res/layout/activity_fragment_container_toolbar.xml new file mode 100644 index 0000000000000000000000000000000000000000..442bc6de79f4ecb49910578601cdd48d7b8f6192 --- /dev/null +++ b/repeater-android/src/main/res/layout/activity_fragment_container_toolbar.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + 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" + tools:context=".android.keyagreement.KeyAgreementActivity"> + + <include layout="@layout/toolbar"/> + + <FrameLayout + android:id="@+id/fragmentContainer" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + +</LinearLayout> diff --git a/repeater-android/src/main/res/layout/fragment_error.xml b/repeater-android/src/main/res/layout/fragment_error.xml new file mode 100644 index 0000000000000000000000000000000000000000..b705051ccdd57d7d9b980ae534b3b99efc20023b --- /dev/null +++ b/repeater-android/src/main/res/layout/fragment_error.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.constraint.ConstraintLayout + 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"> + + <android.support.v7.widget.AppCompatImageView + android:id="@+id/errorIcon" + android:layout_width="128dp" + android:layout_height="128dp" + android:layout_marginEnd="8dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:src="@drawable/alerts_and_states_error" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="?attr/colorControlNormal" + tools:ignore="ContentDescription"/> + + <TextView + android:id="@+id/errorTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:text="@string/sorry" + android:textSize="@dimen/text_size_xlarge" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/errorIcon"/> + + <TextView + android:id="@+id/errorMessage" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:textSize="@dimen/text_size_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/errorTitle" + tools:text="@string/qr_code_unsupported"/> + +</android.support.constraint.ConstraintLayout> diff --git a/repeater-android/src/main/res/layout/fragment_keyagreement_id.xml b/repeater-android/src/main/res/layout/fragment_keyagreement_id.xml new file mode 100644 index 0000000000000000000000000000000000000000..56901cc59d7c418257a07a34fe5fbe1d45833e29 --- /dev/null +++ b/repeater-android/src/main/res/layout/fragment_keyagreement_id.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + android:id="@+id/scrollView" + 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="wrap_content" + android:orientation="vertical"> + + <android.support.constraint.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/margin_large"> + + <ImageView + android:id="@+id/diagram" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:paddingBottom="@dimen/margin_large" + android:scaleType="fitCenter" + android:src="@drawable/qr_code_intro" + android:tint="@color/colorPrimary" + app:layout_constraintBottom_toTopOf="@id/explanationImage" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"/> + + <ImageView + android:id="@+id/explanationImage" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:paddingLeft="@dimen/margin_large" + android:paddingRight="@dimen/margin_large" + android:paddingTop="@dimen/margin_large" + android:scaleType="fitCenter" + android:src="@drawable/qr_code_explanation" + app:layout_constraintBottom_toTopOf="@id/explanationText" + app:layout_constraintEnd_toEndOf="@id/diagram" + app:layout_constraintStart_toStartOf="@id/diagram" + app:layout_constraintTop_toBottomOf="@id/diagram" + tools:ignore="ContentDescription"/> + + <TextView + android:id="@+id/explanationText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="@dimen/margin_large" + android:text="@string/face_to_face" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="@id/explanationImage" + app:layout_constraintStart_toStartOf="@id/explanationImage" + app:layout_constraintTop_toBottomOf="@id/explanationImage"/> + + <View + android:id="@+id/explanationBorder" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/border_explanation" + app:layout_constraintBottom_toBottomOf="@id/explanationText" + app:layout_constraintEnd_toEndOf="@id/explanationImage" + app:layout_constraintStart_toStartOf="@id/explanationImage" + app:layout_constraintTop_toTopOf="@id/explanationImage"/> + + <Button + android:id="@+id/continueButton" + style="@style/BriarButton.Default" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_medium" + android:text="@string/continue_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/explanationImage" + app:layout_constraintStart_toStartOf="@id/explanationImage" + app:layout_constraintTop_toBottomOf="@id/explanationText"/> + + </android.support.constraint.ConstraintLayout> + +</ScrollView> \ No newline at end of file diff --git a/repeater-android/src/main/res/layout/fragment_keyagreement_qr.xml b/repeater-android/src/main/res/layout/fragment_keyagreement_qr.xml new file mode 100644 index 0000000000000000000000000000000000000000..d1c991c9d154a0f6b74518277b4cbf1ce733452f --- /dev/null +++ b/repeater-android/src/main/res/layout/fragment_keyagreement_qr.xml @@ -0,0 +1,52 @@ +<?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.repeater.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:baselineAligned="false" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/status_container" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical" + android:padding="@dimen/margin_medium" + android:visibility="invisible" + tools:visibility="visible"> + + <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> + + <org.briarproject.repeater.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"/> + </LinearLayout> +</FrameLayout> diff --git a/repeater-android/src/main/res/layout/qr_code_view.xml b/repeater-android/src/main/res/layout/qr_code_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..9062428d0e6b3456b06b1e91a8e952cd3963e44d --- /dev/null +++ b/repeater-android/src/main/res/layout/qr_code_view.xml @@ -0,0 +1,39 @@ +<?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/fragment_keyagreement_qr"> + + <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"/> + + <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>