diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..39fb081
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..96cc43e
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..e7bedf3
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..7ac24c7
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..5d19981
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..fe96637
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..af3ab73
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,50 @@
+language: android
+jdk: oraclejdk8
+cache:
+ directories:
+ - node_modules
+sudo: false
+before_install:
+ - mkdir "$ANDROID_HOME/licenses" || true
+ - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
+ - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
+android:
+ components:
+ # Uncomment the lines below if you want to
+ # use the latest revision of Android SDK Tools
+ - tools
+ - platform-tools
+
+ # The BuildTools version used by your project
+ - build-tools-25.0.2
+
+ # The SDK version used to compile your project
+ - android-21
+ - android-25
+
+ # Additional components (included from example)
+ - extra-google-google_play_services
+ - extra-google-m2repository
+ - extra-android-m2repository
+ - addon-google_apis-google-19
+
+ # Specify at least one system image,
+ # if you need to run emulator(s) during your tests
+ - sys-img-armeabi-v7a-android-21
+ licenses:
+ - android-sdk-license-.+
+ - '.+'
+env:
+ global:
+ # install timeout in minutes (2 minutes by default)
+ - ADB_INSTALL_TIMEOUT=8
+
+# Emulator Management: Create, Start and Wait
+before_script:
+ - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a
+ - emulator -avd test -no-skin -no-audio -no-window &
+ - android-wait-for-emulator
+ - adb shell input keyevent 82 &
+script:
+ - android list target
+ - ./gradlew connectedAndroidTest
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b1ed7bd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+[![Build Status](https://travis-ci.org/digital-voting-pass/digital-voting-pass-app.svg?branch=develop)](https://travis-ci.org/digital-voting-pass/digital-voting-pass-app)
+
+# digital-voting-pass-app
+Voting station app to redeem the token on the blockchain using a machine readable travel document.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..2bd5c7c
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,42 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.2"
+ defaultConfig {
+ applicationId "com.digitalvotingpass.digitalvotingpass"
+ minSdkVersion 21
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+
+
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:design:25.3.1'
+ compile 'com.android.support:design:25.3.1'
+ compile 'com.android.support:appcompat-v7:25.3.1'
+ compile 'com.android.support.constraint:constraint-layout:1.0.2'
+ compile 'com.android.support:support-v4:25.3.1'
+ compile 'com.android.support:support-v13:25.3.1'
+ compile 'com.android.support:cardview-v7:25.3.1'
+ compile 'com.rmtheis:tess-two:6.3.0'
+ compile 'org.jmrtd:jmrtd:0.5.5'
+ compile 'com.madgag.spongycastle:prov:1.54.0.0'
+ compile 'net.sf.scuba:scuba-sc-android:0.0.9'
+ testCompile 'junit:junit:4.12'
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..a7ead28
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/jonathan/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..fbbba50
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/eng.traineddata b/app/src/main/assets/eng.traineddata
new file mode 100644
index 0000000..561883f
Binary files /dev/null and b/app/src/main/assets/eng.traineddata differ
diff --git a/app/src/main/assets/fonts/ro.ttf b/app/src/main/assets/fonts/ro.ttf
new file mode 100644
index 0000000..0b14cbc
Binary files /dev/null and b/app/src/main/assets/fonts/ro.ttf differ
diff --git a/app/src/main/assets/mrz.traineddata b/app/src/main/assets/mrz.traineddata
new file mode 100644
index 0000000..7ad31ea
Binary files /dev/null and b/app/src/main/assets/mrz.traineddata differ
diff --git a/app/src/main/java/com/digitalvotingpass/camera/AutoFitTextureView.java b/app/src/main/java/com/digitalvotingpass/camera/AutoFitTextureView.java
new file mode 100644
index 0000000..41ee268
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/camera/AutoFitTextureView.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.digitalvotingpass.camera;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.TextureView;
+
+/**
+ * A {@link TextureView} that can be adjusted to a specified aspect ratio.
+ */
+public class AutoFitTextureView extends TextureView {
+
+ private int mRatioWidth = 0;
+ private int mRatioHeight = 0;
+
+ public AutoFitTextureView(Context context) {
+ this(context, null);
+ }
+
+ public AutoFitTextureView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
+ * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
+ * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
+ *
+ * @param width Relative horizontal size
+ * @param height Relative vertical size
+ */
+ public void setAspectRatio(int width, int height) {
+ if (width < 0 || height < 0) {
+ throw new IllegalArgumentException("Size cannot be negative.");
+ }
+ mRatioWidth = width;
+ mRatioHeight = height;
+ requestLayout();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ if (0 == mRatioWidth || 0 == mRatioHeight) {
+ setMeasuredDimension(width, height);
+ } else {
+ if (width < height * mRatioWidth / mRatioHeight) {
+ setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
+ } else {
+ setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/digitalvotingpass/camera/Camera2BasicFragment.java b/app/src/main/java/com/digitalvotingpass/camera/Camera2BasicFragment.java
new file mode 100644
index 0000000..e626eed
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/camera/Camera2BasicFragment.java
@@ -0,0 +1,946 @@
+package com.digitalvotingpass.camera;/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.graphics.Typeface;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+import android.util.Size;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.digitalvotingpass.digitalvotingpass.MainActivity;
+import com.digitalvotingpass.digitalvotingpass.ManualInputActivity;
+import com.digitalvotingpass.digitalvotingpass.R;
+import com.digitalvotingpass.ocrscanner.Mrz;
+import com.digitalvotingpass.ocrscanner.TesseractOCR;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class Camera2BasicFragment extends Fragment
+ implements FragmentCompat.OnRequestPermissionsResultCallback {
+
+ private static final int DELAY_BETWEEN_OCR_THREADS_MILLIS = 500;
+
+ private ImageView scanSegment;
+ private Overlay overlay;
+ private Button manualInput;
+ private TextView infoText;
+ private View controlPanel;
+
+ private List tesseractThreads = new ArrayList<>();
+ /**
+ * If this is defined and > 0, use this amount of threads instead of dynamically determined amount.
+ */
+ private int ocrThreadNumberOverride;
+
+ /**
+ * Conversion from screen rotation to JPEG orientation.
+ */
+ private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+ private static final int REQUEST_CAMERA_PERMISSION = 1;
+ private static final int REQUEST_WRITE_PERMISSIONS = 3;
+
+ private static final String FRAGMENT_DIALOG = "dialog";
+
+ boolean mIsStateAlreadySaved = false;
+ boolean mPendingShowDialog = false;
+
+ static {
+ ORIENTATIONS.append(Surface.ROTATION_0, 90);
+ ORIENTATIONS.append(Surface.ROTATION_90, 0);
+ ORIENTATIONS.append(Surface.ROTATION_180, 270);
+ ORIENTATIONS.append(Surface.ROTATION_270, 180);
+ }
+
+ // listener for detecting orientation changes
+ private OrientationEventListener orientationListener = null;
+
+ /**
+ * Tag for the {@link Log}.
+ */
+ private static final String TAG = "Camera2BasicFragment";
+
+ /**
+ * Max preview width that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_WIDTH = 1920;
+
+ /**
+ * Max preview height that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_HEIGHT = 1080;
+
+ /**
+ * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a
+ * {@link TextureView}.
+ */
+ private final TextureView.SurfaceTextureListener mSurfaceTextureListener
+ = new TextureView.SurfaceTextureListener() {
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
+ orientationListener.enable();
+ openCamera(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
+ configureTransform(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
+ orientationListener.disable();
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture texture) {
+ }
+ };
+
+ /**
+ * ID of the current {@link CameraDevice}.
+ */
+ private String mCameraId;
+
+ /**
+ * An {@link AutoFitTextureView} for camera preview.
+ */
+ private AutoFitTextureView mTextureView;
+
+ /**
+ * A {@link CameraCaptureSession } for camera preview.
+ */
+ private CameraCaptureSession mCaptureSession;
+
+ /**
+ * A reference to the opened {@link CameraDevice}.
+ */
+ private CameraDevice mCameraDevice;
+
+ /**
+ * The {@link android.util.Size} of camera preview.
+ */
+ private Size mPreviewSize;
+ private boolean resultFound = false;
+
+ private float secTillScanTimeout = 10;
+ private Runnable scanningTakingLongTimeout = new Runnable() {
+ @Override
+ public void run() {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ manualInput.setVisibility(View.VISIBLE);
+ overlay.setMargins(0,0,0,infoText.getHeight() + manualInput.getHeight());
+ }
+ });
+ }
+ };
+
+ /**
+ * Method for delivering correct MRZ when found. This method returns the MRZ as result data and
+ * then exits the activity. This method is synchronized and checks for a boolean to make sure
+ * it is only executed once in this fragments lifetime.
+ * @param mrz Mrz
+ */
+ public synchronized void scanResultFound(final Mrz mrz) {
+ if (!resultFound) {
+ Intent returnIntent = new Intent();
+ returnIntent.putExtra("result", mrz.getPrettyData());
+ getActivity().setResult(Activity.RESULT_OK, returnIntent);
+ resultFound = true;
+ finishActivity();
+ }
+ }
+
+ private void finishActivity() {
+ getActivity().finish();
+ }
+
+ /**
+ * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
+ */
+ private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
+
+ @Override
+ public void onOpened(@NonNull CameraDevice cameraDevice) {
+ // This method is called when the camera is opened. We start camera preview here.
+ mCameraOpenCloseLock.release();
+ mCameraDevice = cameraDevice;
+ createCameraPreviewSession();
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ }
+
+ @Override
+ public void onError(@NonNull CameraDevice cameraDevice, int error) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ Activity activity = getActivity();
+ if (null != activity) {
+ activity.finish();
+ }
+ }
+ };
+
+ /**
+ * An additional thread for running tasks that shouldn't block the UI.
+ */
+ private HandlerThread mBackgroundThread;
+
+ /**
+ * A {@link Handler} for running tasks in the background.
+ * Runs camera preview updater.
+ */
+ private Handler mBackgroundHandler;
+
+ /**
+ * {@link CaptureRequest.Builder} for the camera preview
+ */
+ private CaptureRequest.Builder mPreviewRequestBuilder;
+
+ /**
+ * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder}
+ */
+ private CaptureRequest mPreviewRequest;
+
+ /**
+ * A {@link Semaphore} to prevent the app from exiting before closing the camera.
+ */
+ private Semaphore mCameraOpenCloseLock = new Semaphore(1);
+
+ /**
+ * Whether the current camera device supports Flash or not.
+ */
+ private boolean mFlashSupported;
+
+ /**
+ * Orientation of the camera sensor
+ */
+ private int mSensorOrientation;
+
+ /**
+ * Shows a {@link Toast} on the UI thread.
+ *
+ * @param text The message to show
+ */
+ private void showToast(final String text) {
+ final Activity activity = getActivity();
+ if (activity != null) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, text, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ /**
+ * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
+ *
+ * @param choices The list of sizes that the camera supports for the intended output
+ * class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
+ * @return The optimal {@code Size}, or an arbitrary one if none were big enough
+ */
+ private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
+
+ // Collect the supported resolutions that are at least as big as the preview Surface
+ List bigEnough = new ArrayList<>();
+ // Collect the supported resolutions that are smaller than the preview Surface
+ List notBigEnough = new ArrayList<>();
+ int w = aspectRatio.getWidth();
+ int h = aspectRatio.getHeight();
+ for (Size option : choices) {
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
+ option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth &&
+ option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
+ }
+ }
+
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
+ if (bigEnough.size() > 0) {
+ return Collections.min(bigEnough, new CompareSizesByArea());
+ } else if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, new CompareSizesByArea());
+ } else {
+ Log.e(TAG, "Couldn't find any suitable preview size");
+ return choices[0];
+ }
+ }
+
+ public static Camera2BasicFragment newInstance() {
+ return new Camera2BasicFragment();
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ orientationListener = new OrientationEventListener(this.getActivity()) {
+ public void onOrientationChanged(int orientation) {
+ configureTransform(mTextureView.getWidth(), mTextureView.getHeight());
+ }
+ };
+ int threadsToStart = Runtime.getRuntime().availableProcessors() / 2;
+ if (ocrThreadNumberOverride > 0) {
+ threadsToStart = ocrThreadNumberOverride;
+ }
+ createOCRThreads(threadsToStart);
+ }
+
+ private void createOCRThreads(int amount) {
+ for (int i = 0; i < amount; i++) {
+ tesseractThreads.add(new TesseractOCR("Thread no " + i, this, getActivity().getAssets()));
+ }
+ Log.e(TAG, "Running threads: " + amount);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_camera2_basic, container, false);
+ }
+
+ @Override
+ public void onViewCreated(final View view, final Bundle savedInstanceState) {
+ mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
+ scanSegment = (ImageView) view.findViewById(R.id.scan_segment);
+ manualInput = (Button) view.findViewById(R.id.manual_input_button);
+ overlay = (Overlay) view.findViewById(R.id.overlay);
+ manualInput.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), ManualInputActivity.class);
+ getActivity().startActivityForResult(intent, MainActivity.GET_DOC_INFO);
+ }
+ });
+ infoText = (TextView) view.findViewById(R.id.info_text);
+ Typeface typeFace= Typeface.createFromAsset(getActivity().getAssets(), "fonts/ro.ttf");
+ infoText.setTypeface(typeFace);
+ manualInput.setTypeface(typeFace);
+ controlPanel = view.findViewById(R.id.control);
+ final ViewTreeObserver observer= view.findViewById(R.id.control).getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // Set the margins when the view is available.
+ overlay.setMargins(0, 0, 0, controlPanel.getHeight());
+ view.findViewById(R.id.control).getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startBackgroundThread();
+ // When the screen is turned off and turned back on, the SurfaceTexture is already
+ // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
+ // a camera and start preview from here (otherwise, we wait until the surface is ready in
+ // the SurfaceTextureListener).
+ if (mTextureView.isAvailable()) {
+ openCamera(mTextureView.getWidth(), mTextureView.getHeight());
+ } else {
+ mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
+ }
+ mIsStateAlreadySaved = false;
+ if(mPendingShowDialog){
+ mPendingShowDialog = false;
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ showInfoDialog(R.string.ocr_camera_permission_explanation);
+ } else if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ showInfoDialog(R.string.ocr_storage_permission_explanation);
+ }
+ } else {
+ startTesseractThreads();
+ }
+ }
+
+ private void showInfoDialog(int stringid) {
+ ErrorDialog.newInstance(getString(stringid))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+
+ @Override
+ public void onPause() {
+ closeCamera();
+ stopBackgroundThread();
+ stopTesseractThreads();
+ mIsStateAlreadySaved = true;
+ super.onPause();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+
+ private void requestCameraPermission() {
+ if (FragmentCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
+ ErrorDialog.newInstance(getString(R.string.ocr_camera_permission_explanation))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ } else {
+ FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
+ REQUEST_CAMERA_PERMISSION);
+ }
+ }
+
+ private void requestStoragePermissions() {
+ if (FragmentCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ ErrorDialog.newInstance(getString(R.string.ocr_storage_permission_explanation))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ } else {
+ FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_PERMISSIONS);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_CAMERA_PERMISSION) {
+ if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ if(mIsStateAlreadySaved){
+ mPendingShowDialog = true;
+ } else {
+ ErrorDialog.newInstance(getString(R.string.ocr_camera_permission_explanation))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+ }
+ } else if (requestCode == REQUEST_WRITE_PERMISSIONS) {
+ if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ if(mIsStateAlreadySaved){
+ mPendingShowDialog = true;
+ } else {
+ ErrorDialog.newInstance(getString(R.string.ocr_storage_permission_explanation))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ /**
+ * Sets up member variables related to camera.
+ *
+ * @param width The width of available size for camera preview
+ * @param height The height of available size for camera preview
+ */
+ private void setUpCameraOutputs(int width, int height) {
+ Activity activity = getActivity();
+ CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ for (String cameraId : manager.getCameraIdList()) {
+ CameraCharacteristics characteristics
+ = manager.getCameraCharacteristics(cameraId);
+
+ // We don't use a front facing camera in this sample.
+ Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
+ if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
+ continue;
+ }
+
+ StreamConfigurationMap map = characteristics.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ if (map == null) {
+ continue;
+ }
+
+ // For still image captures, we use the largest available size.
+ Size largest = Collections.max(
+ Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
+ new CompareSizesByArea());
+
+ // Find out if we need to swap dimension to get the preview size relative to sensor
+ // coordinate.
+ int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ //noinspection ConstantConditions
+ mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ boolean swappedDimensions = false;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_90:
+ if (mSensorOrientation == 90 || mSensorOrientation == 270) {
+ swappedDimensions = true;
+ }
+ break;
+ case Surface.ROTATION_180:
+ case Surface.ROTATION_270:
+ if (mSensorOrientation == 0 || mSensorOrientation == 180) {
+ swappedDimensions = true;
+ }
+ break;
+ default:
+ Log.e(TAG, "Display rotation is invalid: " + displayRotation);
+ }
+
+ Point displaySize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
+ int rotatedPreviewWidth = width;
+ int rotatedPreviewHeight = height;
+ int maxPreviewWidth = displaySize.x;
+ int maxPreviewHeight = displaySize.y;
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height;
+ rotatedPreviewHeight = width;
+ maxPreviewWidth = displaySize.y;
+ maxPreviewHeight = displaySize.x;
+ }
+
+ if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+ maxPreviewWidth = MAX_PREVIEW_WIDTH;
+ }
+
+ if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+ maxPreviewHeight = MAX_PREVIEW_HEIGHT;
+ }
+ // Danger, W.R.! Attempting to use too large a preview size could exceed the camera
+ // bus' bandwidth limitation, resulting in gorgeous previews but the storage of
+ // garbage capture data.
+ mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+ rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
+ maxPreviewHeight, largest);
+
+ int orientation = getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ mTextureView.setAspectRatio(
+ mPreviewSize.getWidth(), mPreviewSize.getHeight());
+ } else {
+ //TODO find out why and how this +150 removes the black vertical bar
+ mTextureView.setAspectRatio(
+ mPreviewSize.getHeight(), mPreviewSize.getWidth());
+ }
+
+ // Check if the flash is supported.
+ Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ mFlashSupported = available == null ? false : available;
+
+ mCameraId = cameraId;
+ return;
+ }
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ // Currently an NPE is thrown when the Camera2API is used but not supported on the
+ // device this code runs.
+ ErrorDialog.newInstance(getString(R.string.ocr_camera_error))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+ }
+
+ /**
+ * Opens the camera specified by {@link Camera2BasicFragment#mCameraId}.
+ */
+ private void openCamera(int width, int height) {
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestCameraPermission();
+ return;
+ }
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Cant start Camera due to no data permissions.");
+ return;
+ }
+ setUpCameraOutputs(width, height);
+ configureTransform(width, height);
+ Activity activity = getActivity();
+ CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Time out waiting to lock camera opening.");
+ }
+ manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
+ }
+ }
+
+ /**
+ * Closes the current {@link CameraDevice}.
+ */
+ private void closeCamera() {
+ try {
+ mCameraOpenCloseLock.acquire();
+ if (null != mCaptureSession) {
+ mCaptureSession.close();
+ mCaptureSession = null;
+ }
+ if (null != mCameraDevice) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
+ } finally {
+ mCameraOpenCloseLock.release();
+ }
+ }
+
+ /**
+ * Starts a background thread and its {@link Handler}.
+ */
+ private void startBackgroundThread() {
+ mBackgroundThread = new HandlerThread("CameraBackground");
+ mBackgroundThread.start();
+ mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+ mBackgroundHandler.postDelayed(scanningTakingLongTimeout, (long) (secTillScanTimeout * 1000));
+ }
+
+ private void startTesseractThreads() {
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestStoragePermissions();
+ return;
+ }
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Cant start OCR due to no camera permissions.");
+ return;
+ }
+ int i = 0;
+ for(TesseractOCR ocr : tesseractThreads) {
+ ocr.initialize();
+ ocr.startScanner(i);
+ i += DELAY_BETWEEN_OCR_THREADS_MILLIS;
+ }
+ }
+
+ /**
+ * Stops the background thread and its {@link Handler}.
+ */
+ private void stopBackgroundThread() {
+ mBackgroundHandler.removeCallbacks(scanningTakingLongTimeout);
+ mBackgroundThread.quitSafely();
+ try {
+ mBackgroundThread.join();
+ mBackgroundThread = null;
+ mBackgroundHandler = null;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void stopTesseractThreads() {
+ for (TesseractOCR ocr : tesseractThreads) {
+ ocr.stopScanner();
+ }
+ }
+
+ /**
+ * Creates a new {@link CameraCaptureSession} for camera preview.
+ */
+ private void createCameraPreviewSession() {
+ try {
+ SurfaceTexture texture = mTextureView.getSurfaceTexture();
+ assert texture != null;
+
+ // We configure the size of default buffer to be the size of camera preview we want.
+ texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+
+ // This is the output Surface we need to start preview.
+ Surface surface = new Surface(texture);
+
+ // We set up a CaptureRequest.Builder with the output Surface.
+ mPreviewRequestBuilder
+ = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ mPreviewRequestBuilder.addTarget(surface);
+ // Here, we create a CameraCaptureSession for camera preview.
+ mCameraDevice.createCaptureSession(Arrays.asList(surface),
+ new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
+ // The camera is already closed
+ if (null == mCameraDevice) {
+ return;
+ }
+ // When the session is ready, we start displaying the preview.
+ mCaptureSession = cameraCaptureSession;
+ try {
+ // Auto focus should be continuous for camera preview.
+ mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ // Flash is automatically enabled when necessary.
+ setAutoFlash(mPreviewRequestBuilder);
+
+ // Finally, we start displaying the camera preview.
+ mPreviewRequest = mPreviewRequestBuilder.build();
+ if (!mIsStateAlreadySaved)
+ mCaptureSession.setRepeatingRequest(mPreviewRequest,
+ null, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(
+ @NonNull CameraCaptureSession cameraCaptureSession) {
+ showToast("Failed");
+ }
+ }, null
+ );
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`.
+ * This method should be called after the camera preview size is determined in
+ * setUpCameraOutputs and also the size of `mTextureView` is fixed.
+ *
+ * @param viewWidth The width of `mTextureView`
+ * @param viewHeight The height of `mTextureView`
+ */
+ private void configureTransform(int viewWidth, int viewHeight) {
+ Activity activity = getActivity();
+ if (null == mTextureView || null == mPreviewSize || null == activity) {
+ return;
+ }
+ int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ Matrix matrix = new Matrix();
+ RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
+ RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
+ float centerX = viewRect.centerX();
+ float centerY = viewRect.centerY();
+ if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
+ bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
+ matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
+ float scale = Math.max(
+ (float) viewHeight / mPreviewSize.getHeight(),
+ (float) viewWidth / mPreviewSize.getWidth());
+ matrix.postScale(scale, scale, centerX, centerY);
+ matrix.postRotate(90 * (rotation - 2), centerX, centerY);
+ } else if (Surface.ROTATION_180 == rotation) {
+ matrix.postRotate(180, centerX, centerY);
+ }
+ mTextureView.setTransform(matrix);
+ int startX = (int) scanSegment.getX();
+ int startY = (int) scanSegment.getY();
+ int width = (scanSegment.getWidth());
+ int length = (scanSegment.getHeight());
+ overlay.setRect(new Rect(startX, startY, startX + width, startY+length));
+ }
+
+ private void setAutoFlash(CaptureRequest.Builder requestBuilder) {
+ if (mFlashSupported) {
+ requestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ }
+ }
+
+ /**
+ * Compares two {@code Size}s based on their areas.
+ */
+ static class CompareSizesByArea implements Comparator {
+
+ @Override
+ public int compare(Size lhs, Size rhs) {
+ // We cast here to ensure the multiplications won't overflow
+ return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
+ (long) rhs.getWidth() * rhs.getHeight());
+ }
+
+ }
+
+ /**
+ * Shows an error message dialog.
+ */
+ public static class ErrorDialog extends DialogFragment {
+
+ private static final String ARG_MESSAGE = "message";
+
+ public static ErrorDialog newInstance(String message) {
+ ErrorDialog dialog = new ErrorDialog();
+ Bundle args = new Bundle();
+ args.putString(ARG_MESSAGE, message);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ return new AlertDialog.Builder(activity)
+ .setMessage(getArguments().getString(ARG_MESSAGE))
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ activity.finish();
+ }
+ })
+ .create();
+ }
+
+ }
+
+ public Bitmap extractBitmap() {
+ try {
+ Bitmap bitmap = mTextureView.getBitmap();
+ int rotate = 0;
+ switch (getActivity().getWindowManager().getDefaultDisplay().getRotation()) {
+ case 0:
+ rotate = 0;
+ break;
+ case 1:
+ rotate = 270;
+ break;
+ case 2:
+ rotate = 180;
+ break;
+ case 3:
+ rotate = 90;
+ break;
+ }
+ if (rotate != 0) {
+ bitmap = rotateBitmap(bitmap, rotate);
+ }
+ Bitmap croppedBitmap = cropBitmap(bitmap);
+
+ final Bitmap b = getResizedBitmap(croppedBitmap, (int) (croppedBitmap.getWidth()), (int) (croppedBitmap.getHeight()));
+
+ return b;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ // Setting pre rotate
+ Matrix mtx = new Matrix();
+ mtx.preRotate(degrees);
+ // Rotating Bitmap
+ Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
+ return Bitmap.createScaledBitmap(rotated, bitmap.getWidth(), bitmap.getHeight(), true);
+ }
+
+ public Bitmap getResizedBitmap(Bitmap bm, int newWidth, int newHeight) {
+ int width = bm.getWidth();
+ int height = bm.getHeight();
+ float scaleWidth = ((float) newWidth) / width;
+ float scaleHeight = ((float) newHeight) / height;
+ // CREATE A MATRIX FOR THE MANIPULATION
+ Matrix matrix = new Matrix();
+ // RESIZE THE BIT MAP
+ matrix.postScale(scaleWidth, scaleHeight);
+
+ // "RECREATE" THE NEW BITMAP
+ Bitmap resizedBitmap = Bitmap.createBitmap(
+ bm, 0, 0, width, height, matrix, false);
+ bm.recycle();
+ return resizedBitmap;
+ }
+
+ private Bitmap cropBitmap(Bitmap bitmap) {
+ int startX = (int) scanSegment.getX();
+ int startY = (int) scanSegment.getY();
+ int width = (int) (scanSegment.getWidth());
+ int length = (int) (scanSegment.getHeight());
+ return Bitmap.createBitmap(bitmap, startX, startY, width > bitmap.getWidth() ? bitmap.getWidth() : width, length);
+ //TODO fix the need for inline if, if the aspect ratio causes a vertical bar it will crash otherwise.
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/camera/CameraActivity.java b/app/src/main/java/com/digitalvotingpass/camera/CameraActivity.java
new file mode 100644
index 0000000..bc84493
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/camera/CameraActivity.java
@@ -0,0 +1,59 @@
+package com.digitalvotingpass.camera;/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v13.app.ActivityCompat;
+
+import com.digitalvotingpass.digitalvotingpass.MainActivity;
+import com.digitalvotingpass.digitalvotingpass.R;
+
+import java.util.HashMap;
+
+public class CameraActivity extends Activity {
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera);
+ if (null == savedInstanceState) {
+ getFragmentManager().beginTransaction()
+ .replace(R.id.container, Camera2BasicFragment.newInstance())
+ .commit();
+ }
+ }
+
+ /**
+ * Pass data from ManualInputActivity to the MainActivity.
+ * @param requestCode requestCode
+ * @param resultCode resultCode
+ * @param data The data
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == MainActivity.GET_DOC_INFO && resultCode == RESULT_OK) {
+ setResult(Activity.RESULT_OK, data);
+ //Clear this activity
+ finish();
+ }
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/camera/Overlay.java b/app/src/main/java/com/digitalvotingpass/camera/Overlay.java
new file mode 100644
index 0000000..bc00607
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/camera/Overlay.java
@@ -0,0 +1,60 @@
+package com.digitalvotingpass.camera;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.digitalvotingpass.digitalvotingpass.R;
+
+/**
+ * Creates a grey overlay with a rectangular transparent field, set by setRect()
+ */
+public class Overlay extends View {
+ public static final String TAG = "Overlay";
+
+ /**
+ * Transparency of overlaid part in hex, 0-255
+ */
+ private static final int TRANSPARENCY = 0xA0;
+
+ private Paint paint = new Paint();
+ private Paint transparentPaint = new Paint();
+ private Rect rect = new Rect(0,0,0,0);
+ private PorterDuffXfermode xfermode;
+
+ public Overlay(Context context, AttributeSet set) {
+ super(context, set);
+ xfermode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
+ }
+
+ public void setMargins (int left, int top, int right, int bottom) {
+ if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
+ ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) getLayoutParams();
+ p.setMargins(left, top, right, bottom);
+ requestLayout();
+ }
+ }
+
+ public void setRect(Rect rect) {
+ this.rect = rect;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ // The color I chose is just a random existing grey color
+ paint.setColor(getResources().getColor(R.color.cardview_dark_background));
+ paint.setAlpha(TRANSPARENCY);
+ canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
+
+ transparentPaint.setAlpha(0xFF);
+ transparentPaint.setXfermode(xfermode);
+ canvas.drawRect(rect, transparentPaint);
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/digitalvotingpass/MainActivity.java b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/MainActivity.java
new file mode 100644
index 0000000..995608c
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/MainActivity.java
@@ -0,0 +1,94 @@
+package com.digitalvotingpass.digitalvotingpass;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.digitalvotingpass.electionchoice.Election;
+import com.digitalvotingpass.camera.CameraActivity;
+import com.digitalvotingpass.passportconnection.PassportConActivity;
+
+import java.util.HashMap;
+
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "Main activity";
+ public HashMap documentData = new HashMap<>();
+ public Election election;
+
+ private Button manualInput;
+ private Button startOCR;
+
+ public static final int GET_DOC_INFO = 1;
+
+ public static final String DOCUMENT_NUMBER = "Document Number";
+ public static final String DATE_OF_BIRTH = "Date of Birth";
+ public static final String EXPIRATION_DATE = "Expiration Date";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle extras = getIntent().getExtras();
+ election = (Election) extras.get("election");
+
+ final MainActivity thisActivity = this;
+ setContentView(R.layout.activity_main);
+ Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
+ setSupportActionBar(appBar);
+ // set the text of the appbar to the selected election
+
+ getSupportActionBar().setTitle(election.getKind());
+ getSupportActionBar().setSubtitle(election.getPlace());
+
+ manualInput = (Button) findViewById(R.id.manual_input_button);
+ manualInput.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(thisActivity, ManualInputActivity.class);
+ // send the docData to the manualinput in case a user wants to edit the existing docdata
+ intent.putExtra("docData", documentData);
+ startActivityForResult(intent, GET_DOC_INFO);
+ }
+ });
+
+ startOCR = (Button) findViewById(R.id.start_ocr);
+ startOCR.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(thisActivity, CameraActivity.class);
+ startActivityForResult(intent, GET_DOC_INFO);
+ }
+ });
+ }
+
+ /**
+ * Called when the user taps the "Start Reading ID" button
+ */
+ public void startReading(View view) {
+ if(documentData.isEmpty()) {
+ Toast.makeText(this,R.string.scan_doc_details, Toast.LENGTH_LONG).show();
+ Intent intent = new Intent(this, CameraActivity.class);
+ startActivityForResult(intent, GET_DOC_INFO);
+ } else {
+ Intent intent = new Intent(this, PassportConActivity.class);
+ intent.putExtra("docData", documentData);
+ startActivity(intent);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ // Both switches do the same thing, this might change in the future
+ if(requestCode == GET_DOC_INFO){
+ if (resultCode == RESULT_OK) {
+ documentData = (HashMap) data.getSerializableExtra("result");
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/digitalvotingpass/digitalvotingpass/ManualInputActivity.java b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/ManualInputActivity.java
new file mode 100644
index 0000000..90d7bd9
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/ManualInputActivity.java
@@ -0,0 +1,200 @@
+package com.digitalvotingpass.digitalvotingpass;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+
+public class ManualInputActivity extends AppCompatActivity {
+ private EditText docNumber;
+
+ Spinner dobDaySpinner;
+ Spinner dobMonthSpinner;
+ Spinner dobYearSpinner;
+
+ Spinner expiryDaySpinner;
+ Spinner expiryMonthSpinner;
+ Spinner expiryYearSpinner;
+
+ // Define the length of document details here, because getting maxLength from EditText is complex
+ private final int DOC_NUM_LENGTH = 9;
+ private final int DOB_YEAR_STARTING_INDEX = 60; // Start at 1960
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_manual_input);
+ Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
+ setSupportActionBar(appBar);
+ Typeface typeFace= Typeface.createFromAsset(getAssets(), "fonts/ro.ttf");
+
+ docNumber = (EditText) findViewById(R.id.doc_num);
+ docNumber.setTypeface(typeFace);
+ TextView docNumTitle = (TextView) findViewById(R.id.doc_num_title);
+ TextView dobTitle = (TextView) findViewById(R.id.dob_title);
+ TextView expDateTitle = (TextView) findViewById(R.id.exp_date_title);
+ docNumTitle.setTypeface(typeFace);
+ dobTitle.setTypeface(typeFace);
+ expDateTitle.setTypeface(typeFace);
+
+ Button submitBut = (Button) findViewById(R.id.submit_button);
+ submitBut.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if(verifyInput()) {
+ Intent returnIntent = new Intent();
+ returnIntent.putExtra("result", getData());
+ setResult(Activity.RESULT_OK, returnIntent);
+ finish();
+ }
+ }
+ });
+ setupDOBSpinners();
+ setupExpirySpinners();
+
+ // When docData was previously filled in, update text fields
+ if(getIntent().hasExtra("docData")) {
+ putData(getIntent().getExtras());
+ }
+ }
+
+ private void setupExpirySpinners() {
+ expiryDaySpinner = (Spinner) findViewById(R.id.expiry_day_spinner);
+ expiryMonthSpinner = (Spinner) findViewById(R.id.expiry_month_spinner);
+ expiryYearSpinner = (Spinner) findViewById(R.id.expiry_year_spinner);
+
+ List days = new ArrayList<>();
+ for (int i = 0; i < 31; i++) {
+ days.add("" + (i + 1));
+ }
+
+ // Leave the default view (android.R.layout.simple_spinner_item) but set custom view for dropdown to add extra padding
+ ArrayAdapter dayAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_spinner_item, days);
+ dayAdapter.setDropDownViewResource(R.layout.spinner_dropdown);
+ expiryDaySpinner.setAdapter(dayAdapter);
+
+ ArrayAdapter monthAdapter = ArrayAdapter.createFromResource(this,
+ R.array.months_array, android.R.layout.simple_spinner_item);
+ monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ expiryMonthSpinner.setAdapter(monthAdapter);
+
+ Date dt = new Date();
+ List years = new ArrayList<>();
+ for (int i = 0; i <= 10; i++) {
+ years.add("" + (dt.getYear() + 1900 + i));
+ }
+ ArrayAdapter yearAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_spinner_item, years);
+ yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ expiryYearSpinner.setAdapter(yearAdapter);
+ }
+
+ private void setupDOBSpinners () {
+ dobDaySpinner = (Spinner) findViewById(R.id.dob_day_spinner);
+ dobMonthSpinner = (Spinner) findViewById(R.id.dob_month_spinner);
+ dobYearSpinner = (Spinner) findViewById(R.id.dob_year_spinner);
+
+ List days = new ArrayList<>();
+ for (int i = 0; i < 31; i++) {
+ days.add("" + (i + 1));
+ }
+ ArrayAdapter dayAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_spinner_item, days);
+ dayAdapter.setDropDownViewResource(R.layout.spinner_dropdown);
+ dobDaySpinner.setAdapter(dayAdapter);
+
+ ArrayAdapter monthAdapter = ArrayAdapter.createFromResource(this,
+ R.array.months_array, android.R.layout.simple_spinner_item);
+ monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ dobMonthSpinner.setAdapter(monthAdapter);
+
+ Date dt = new Date();
+ List years = new ArrayList<>();
+ int maxYear = dt.getYear() + 1900 - 18;
+ for (int i = 1900; i <= maxYear; i++) {
+ years.add("" + i);
+ }
+ ArrayAdapter yearAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_spinner_item, years);
+ yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ dobYearSpinner.setAdapter(yearAdapter);
+ dobYearSpinner.setSelection(DOB_YEAR_STARTING_INDEX);
+ }
+
+ /**
+ * Update textFields with docData if it was previously filled in, else leave as is.
+ */
+ public void putData(Bundle extras) {
+ HashMap docData = (HashMap) extras.get("docData");
+ if(docData != null && docData.size() > 0) {
+ docNumber.setText(docData.get(MainActivity.DOCUMENT_NUMBER));
+ dobYearSpinner.setSelection(Integer.parseInt(docData.get(MainActivity.DATE_OF_BIRTH).substring(0,2)));
+ dobMonthSpinner.setSelection(Integer.parseInt(docData.get(MainActivity.DATE_OF_BIRTH).substring(2,4))-1);
+ dobDaySpinner.setSelection(Integer.parseInt(docData.get(MainActivity.DATE_OF_BIRTH).substring(4,6))-1);
+
+ Date dt = new Date();
+ int currYear = dt.getYear() - 100;
+ expiryYearSpinner.setSelection(Integer.parseInt(docData.get(MainActivity.EXPIRATION_DATE).substring(0,2)) - currYear);
+ expiryMonthSpinner.setSelection(Integer.parseInt(docData.get(MainActivity.EXPIRATION_DATE).substring(2,4))-1);
+ expiryDaySpinner.setSelection(Integer.parseInt(docData.get(MainActivity.EXPIRATION_DATE).substring(4,6))-1);
+ }
+ }
+
+ /**
+ * Create a hashmap of the input date in the same way as the OCR scanner does.
+ * @return data - A String Hashmap of the required document data for BAC.
+ */
+ public HashMap getData() {
+ HashMap data = new HashMap<>();
+ DecimalFormat formatter = new DecimalFormat("00");
+ data.put(MainActivity.DOCUMENT_NUMBER, docNumber.getText().toString().toUpperCase());
+ data.put(MainActivity.DATE_OF_BIRTH,
+ dobYearSpinner.getSelectedItem().toString().substring(2) +
+ formatter.format(dobMonthSpinner.getSelectedItemId()+1) +
+ formatter.format(Integer.parseInt(dobDaySpinner.getSelectedItem().toString())));
+
+ data.put(MainActivity.EXPIRATION_DATE,
+ expiryYearSpinner.getSelectedItem().toString().substring(2) +
+ formatter.format(expiryMonthSpinner.getSelectedItemId()+1) +
+ formatter.format(Integer.parseInt(expiryDaySpinner.getSelectedItem().toString())));
+
+ return data;
+ }
+
+ /**
+ * Check if all the fields have been filled in and check for wrong length input.
+ * TODO this does not check if dates exist, e.g no error is given when feb 31st is entered.
+ * @return valid - boolean which indicates whether the input is valid.
+ */
+ public boolean verifyInput() {
+ boolean valid = true;
+ int docNumLength = docNumber.getText().toString().length();
+ if(docNumLength != DOC_NUM_LENGTH ) {
+ valid = false;
+ if(docNumLength == 0) {
+ docNumber.setError(getResources().getString(R.string.errInputDocNum));
+ }
+ else {
+ docNumber.setError(getResources().getString(R.string.errFormatDocNum));
+ }
+ }
+ return valid;
+ }
+
+}
diff --git a/app/src/main/java/com/digitalvotingpass/digitalvotingpass/ResultActivity.java b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/ResultActivity.java
new file mode 100644
index 0000000..78e9bcf
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/ResultActivity.java
@@ -0,0 +1,204 @@
+package com.digitalvotingpass.digitalvotingpass;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.digitalvotingpass.transactionhistory.TransactionHistoryActivity;
+
+public class ResultActivity extends AppCompatActivity {
+ private TextView textAuthorization;
+ private TextView textVoterName;
+ private TextView textVotingPassAmount;
+ private TextView textVotingPasses;
+ private Button butTransactionHistory;
+ private Button butProceed;
+ private MenuItem cancelAction;
+ private int authorizationState = 1;
+ private final int FAILED = 0;
+ private final int WAITING = 1;
+ private final int SUCCES = 2;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ResultActivity thisActivity = this;
+ Bundle extras = getIntent().getExtras();
+
+ setContentView(R.layout.activity_result);
+ Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
+ setSupportActionBar(appBar);
+
+ textAuthorization = (TextView) findViewById(R.id.authorization);
+ textVoterName = (TextView) findViewById(R.id.voter_name);
+ textVotingPassAmount = (TextView) findViewById(R.id.voting_pass_amount);
+ textVotingPasses = (TextView) findViewById(R.id.voting_passes);
+ butTransactionHistory = (Button) findViewById(R.id.transactionHistory);
+ butProceed = (Button) findViewById(R.id.proceed);
+
+ butTransactionHistory.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(thisActivity, TransactionHistoryActivity.class);
+ startActivity(intent);
+ }
+ });
+
+ butProceed.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ proceed();
+ }
+ });
+ }
+
+ /**
+ * Set the result_menu setup to the app bar.
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.result_menu, menu);
+ cancelAction = menu.findItem(R.id.action_cancel);
+ // Start handleData when menu is fully loaded. This method is loaded after onCreate()
+ // TODO: move to a better place
+ Bundle extras = getIntent().getExtras();
+ handleData(extras);
+ return true;
+ }
+
+ /**
+ * Handles the action buttons on the app bar.
+ * In our case it is only one that needs to be handled, the cancel button.
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_cancel:
+ cancelVoting();
+ return true;
+ default:
+ // If we got here, the user's action was not recognized.
+ // Invoke the superclass to handle it.
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * Displays all the data gotten from the blockchain and the passport. Transferred in the extras
+ * field of the intent.
+ *
+ * TODO: handle actual data
+ */
+ public void handleData(Bundle extras) {
+ String name = "Piet Jansen";
+ int votingPasses = 1;
+ int authState = SUCCES;
+
+ textVoterName.setText(getString(R.string.has_right, name));
+ // display singular or plural form of voting passes based on amount
+ if(votingPasses == 1) {
+ textVotingPasses.setText(R.string.voting_pass);
+ } else {
+ textVotingPasses.setText(R.string.voting_passes);
+ }
+ textVotingPassAmount.setText(Integer.toString(votingPasses));
+ setAuthorizationStatus(authState);
+ }
+
+ /**
+ * Sets the textview which displays the authorization status, based on the current state of the
+ * process to one of the following:
+ * - Succesful (transaction was accepted on the blockchain)
+ * - Waiting (waiting for confirmation or rejection of transaction from blockchain)
+ * - Failed (request of balance showed no voting passes left or transaction was rejected)
+ *
+ * Only show cancel button when state is succesful (otherwise nothing to cancel)
+ */
+ public void setAuthorizationStatus(int newState) {
+ //TODO: implement actual conditions for either one of the three states.
+ authorizationState = newState;
+ switch (newState) {
+ case FAILED:
+ textAuthorization.setTextColor(getResources().getColor(R.color.redFailed));
+ textAuthorization.setText(R.string.authorization_failed);
+ textAuthorization.setCompoundDrawablesWithIntrinsicBounds(0,0,R.drawable.not_approve,0);
+ butProceed.setText(R.string.proceed_home);
+ if(cancelAction != null) {
+ cancelAction.setVisible(false);
+ }
+ break;
+ case WAITING:
+ textAuthorization.setTextColor(getResources().getColor(R.color.orangeWait));
+ textAuthorization.setText(R.string.authorization_wait);
+ butProceed.setText(R.string.proceed_home);
+ if(cancelAction != null) {
+ cancelAction.setVisible(true);
+ }
+ break;
+ case SUCCES:
+ textAuthorization.setTextColor(getResources().getColor(R.color.greenSucces));
+ textAuthorization.setText(R.string.authorization_succesful);
+ textAuthorization.setCompoundDrawablesWithIntrinsicBounds(0,0,R.drawable.approve,0);
+ butProceed.setText(R.string.proceed_cast_vote);
+ if(cancelAction != null) {
+ cancelAction.setVisible(true);
+ }
+ break;
+ default:
+ textAuthorization.setTextColor(getResources().getColor(R.color.orangeWait));
+ textAuthorization.setText(R.string.authorization_wait);
+ }
+ }
+
+ /**
+ * Handles which step must be taken next when the proceed button is clicked.
+ * Either calls nextVoter or confirmVote methods, based on the current text in the button.
+ * This would be the action that the user wants to perform.
+ */
+ public void proceed() {
+ String currentText = butProceed.getText().toString();
+ if(currentText.equals(getString(R.string.proceed_home))) {
+ nextVoter();
+ } else if(currentText.equals(getString(R.string.proceed_cast_vote))){
+ confirmVote();
+ }
+ }
+
+ /**
+ * Return to the main activity for starting the process for the next voter.
+ * TODO: implement this method
+ */
+ public void nextVoter() {
+ Intent intent = new Intent(getApplicationContext(), MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ }
+
+ /**
+ * Send the transaction to the blockchain and wait for a confirmation.
+ * TODO: implement this method
+ */
+ public void confirmVote() {
+ Toast.makeText(this, "Transaction sent", Toast.LENGTH_LONG).show();
+ butProceed.setText(R.string.proceed_home);
+ }
+
+ /**
+ * Cancel the voting process for the current voter and return to the mainactivity for starting
+ * a new process for the next voter.
+ * TODO: implement this method
+ */
+ public void cancelVoting() {
+
+ nextVoter();
+ }
+
+}
diff --git a/app/src/main/java/com/digitalvotingpass/digitalvotingpass/SplashActivity.java b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/SplashActivity.java
new file mode 100644
index 0000000..3aedd61
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/digitalvotingpass/SplashActivity.java
@@ -0,0 +1,36 @@
+package com.digitalvotingpass.digitalvotingpass;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+
+import com.digitalvotingpass.electionchoice.ElectionChoiceActivity;
+
+public class SplashActivity extends Activity{
+ // Duration of splash screen in millis
+ private final int SPLASH_DISPLAY_LENGTH = 1000;
+
+ /**
+ * Creates a splash screen
+ * @param bundle
+ */
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.activity_splash_screen);
+
+ //Start MainActivity after SPLASH_DISPLAY_LENGTH
+ //This delay can be removed when we need to actually load data
+ new Handler().postDelayed(new Runnable(){
+ @Override
+ public void run() {
+ // Create an Intent that will start the MainActivity
+ Intent mainIntent = new Intent(SplashActivity.this, ElectionChoiceActivity.class);
+ SplashActivity.this.startActivity(mainIntent);
+ SplashActivity.this.finish();
+ }
+ }, SPLASH_DISPLAY_LENGTH);
+ }
+
+}
diff --git a/app/src/main/java/com/digitalvotingpass/electionchoice/Election.java b/app/src/main/java/com/digitalvotingpass/electionchoice/Election.java
new file mode 100644
index 0000000..edfddcd
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/electionchoice/Election.java
@@ -0,0 +1,80 @@
+package com.digitalvotingpass.electionchoice;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Election implements Parcelable{
+ public String kind;
+ public String place;
+
+ /**
+ * Creates an election object with two attributes.
+ * @param kind - The kind of election. (e.g. municipal, parlement)
+ * @param place - The location of the election. (e.g. Amsterdam, Den Haag, Noord-Brabant)
+ */
+ public Election(String kind, String place) {
+ this.kind = kind;
+ this.place = place;
+ }
+
+ /**
+ * Constructor for parcel
+ * @param in - The parcel containing the data for the election object
+ */
+ public Election(Parcel in){
+ String[] data = new String[2];
+
+ in.readStringArray(data);
+ // the order needs to be the same as in writeToParcel() method
+ this.kind = data[0];
+ this.place = data[1];
+ }
+
+ /**
+ * Getter function for kind attribute of Election object
+ * @return kind - A string depicting the kind of election
+ */
+ public String getKind() {
+ return kind;
+ }
+
+ /**
+ * Getter function for place attribute of Election object
+ * @return place - A string depicting the place of an election
+ */
+ public String getPlace() {
+ return place;
+ }
+
+ /**
+ * Must be overridden, can be ignored.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write the data of the election object to the parcel
+ * @param dest
+ * @param flags
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringArray(new String[] {this.kind,
+ this.place});
+ }
+
+ /**
+ * Used to regenerate the election object.
+ */
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public Election createFromParcel(Parcel in) {
+ return new Election(in);
+ }
+
+ public Election[] newArray(int size) {
+ return new Election[size];
+ }
+ };
+}
diff --git a/app/src/main/java/com/digitalvotingpass/electionchoice/ElectionChoiceActivity.java b/app/src/main/java/com/digitalvotingpass/electionchoice/ElectionChoiceActivity.java
new file mode 100644
index 0000000..8ff059e
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/electionchoice/ElectionChoiceActivity.java
@@ -0,0 +1,104 @@
+package com.digitalvotingpass.electionchoice;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.support.v7.widget.SearchView;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.digitalvotingpass.digitalvotingpass.MainActivity;
+import com.digitalvotingpass.digitalvotingpass.R;
+
+import java.util.ArrayList;
+
+public class ElectionChoiceActivity extends AppCompatActivity implements SearchView.OnQueryTextListener{
+ private ListView electionListView;
+ private ElectionsAdapter electionsAdapter;
+ private MenuItem searchItem;
+ private SearchView searchView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_election_choice);
+ final ElectionChoiceActivity thisActivity = this;
+
+ Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
+ setSupportActionBar(appBar);
+ getSupportActionBar().setTitle(getString(R.string.election_choice));
+
+ electionListView = (ListView) findViewById(R.id.election_list);
+
+ // create a election array with all the elections and add them to the list
+ // TODO: handle actual election choice input data
+ ArrayList electionChoices = new ArrayList<>();
+ electionChoices.add(new Election("Gemeenteraadsverkiezing", "Delft"));
+ electionChoices.add(new Election("Gemeenteraadsverkiezing", "Rotterdam"));
+ electionChoices.add(new Election("Provinciale Statenverkiezing", "Zuid-Holland"));
+
+ electionsAdapter = new ElectionsAdapter(this, electionChoices);
+ electionListView.setAdapter(electionsAdapter);
+
+ electionListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position,
+ long id) {
+ Intent intent = new Intent(thisActivity, MainActivity.class);
+ // Get the election associated with the clicked listItem
+ Election election = (Election) parent.getItemAtPosition(position);
+ intent.putExtra("election", election);
+ startActivity(intent);
+ }
+ });
+ }
+
+ /**
+ * Handles the tasks that need to be performed when the menu is created.
+ * Sets the custom menu layout and sets up the search field handlers.
+ * @param menu
+ * @return
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.choice_menu, menu);
+
+ SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+ searchItem = menu.findItem(R.id.search);
+ searchView = (SearchView) searchItem.getActionView();
+
+ searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
+ searchView.setSubmitButtonEnabled(true);
+ searchView.setOnQueryTextListener(this);
+
+ return true;
+ }
+
+ /**
+ * This method must be overridden.
+ * No use for submitting the text, because list is updated live. Therefore return false.
+ * @param query - the input in the search field
+ * @return false - this method is not used
+ */
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return false;
+ }
+
+ /**
+ * When the input of the search field changes, call the custom filter so the list is updated
+ * @param newText - New input to the search field
+ * @return true - the method is called
+ */
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ electionsAdapter.getFilter().filter(newText);
+ return true;
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/electionchoice/ElectionsAdapter.java b/app/src/main/java/com/digitalvotingpass/electionchoice/ElectionsAdapter.java
new file mode 100644
index 0000000..3c71a86
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/electionchoice/ElectionsAdapter.java
@@ -0,0 +1,137 @@
+package com.digitalvotingpass.electionchoice;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.TextView;
+
+import com.digitalvotingpass.digitalvotingpass.R;
+
+import java.util.ArrayList;
+
+
+public class ElectionsAdapter extends BaseAdapter implements Filterable {
+ private Context context;
+ private ArrayList electionList;
+ private ArrayList filteredList;
+ private ElectionFilter electionFilter;
+
+
+ public ElectionsAdapter(Context context, ArrayList elections) {
+ this.context = context;
+ this.electionList = elections;
+ this.filteredList = elections;
+
+ getFilter();
+ }
+
+ /**
+ * Get size of the filtered electionlist
+ * @return size
+ */
+ @Override
+ public int getCount() {
+ return filteredList.size();
+ }
+
+ /**
+ * Get specific item from the filtered election list
+ * @param i item index
+ * @return list item
+ */
+ @Override
+ public Election getItem(int i) {
+ return filteredList.get(i);
+ }
+
+ /**
+ * Must be overridden, normally returns the id of and item.
+ * In our case the id will be the same as the position.
+ */
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /**
+ * Handle the conversion from the elections object to the list item textviews.
+ * TODO: handle actual election choice data
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Get the data item for this position
+ Election election = getItem(position);
+ // Check if an existing view is being reused, otherwise inflate the view
+ if (convertView == null) {
+ convertView = LayoutInflater.from(context).inflate(R.layout.item_election, parent, false);
+ }
+ // Lookup view for data population
+ TextView kind = (TextView) convertView.findViewById(R.id.kind);
+ TextView place = (TextView) convertView.findViewById(R.id.place);
+ // Populate the data into the template view using the data object
+ kind.setText(election.kind);
+ place.setText(election.place);
+
+ // Return the completed view to render on screen
+ return convertView;
+ }
+
+ /**
+ * Get the custom election filter
+ * @return filter
+ */
+ @Override
+ public Filter getFilter() {
+ if (electionFilter == null) {
+ electionFilter = new ElectionFilter();
+ }
+ return electionFilter;
+ }
+
+
+ /**
+ * Custom filter for election choice list
+ * Filter content in election choice list according to the search text,
+ * filter on both the place and the kind of election
+ */
+ private class ElectionFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults filterResults = new FilterResults();
+ if (constraint != null && constraint.length() > 0) {
+ ArrayList tempList = new ArrayList<>();
+
+ // Match the search input to the kind and place attributes of an election object
+ for (Election election : electionList) {
+ if (election.getKind().toLowerCase().contains(constraint.toString().toLowerCase())
+ || election.getPlace().toLowerCase().contains(constraint.toString().toLowerCase())) {
+ tempList.add(election);
+ }
+ }
+
+ filterResults.count = tempList.size();
+ filterResults.values = tempList;
+ } else {
+ filterResults.count = electionList.size();
+ filterResults.values = electionList;
+ }
+
+ return filterResults;
+ }
+
+ /**
+ * Updates the filteredList and notifies the adapter that the dataset has been changed
+ * so the view is updated.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ filteredList = (ArrayList) results.values;
+ notifyDataSetChanged();
+ }
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/ocrscanner/Mrz.java b/app/src/main/java/com/digitalvotingpass/ocrscanner/Mrz.java
new file mode 100644
index 0000000..0f10fcc
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/ocrscanner/Mrz.java
@@ -0,0 +1,128 @@
+package com.digitalvotingpass.ocrscanner;
+
+import com.digitalvotingpass.digitalvotingpass.MainActivity;
+
+import java.util.HashMap;
+
+/**
+ * Created by jonathan on 5/16/17.
+ */
+
+public class Mrz {
+
+ private static final int[] PASSPORT_DOCNO_INDICES = new int[]{0, 9};
+ private static final int[] PASSPORT_DOB_INDICES = new int[]{13, 19};
+ private static final int[] PASSPORT_EXP_INDICES = new int[]{21, 27};
+
+ private static final int[] ID_DOCNO_INDICES = new int[]{5, 14};
+ private static final int[] ID_DOB_INDICES = new int[]{0, 6};
+ private static final int[] ID_EXP_INDICES = new int[]{8, 14};
+
+ private String mrz;
+
+ public Mrz(String mrz) {
+ this.mrz = mrz;
+ cleanMRZString();
+ }
+
+ /**
+ * Does some basic cleaning on the MRZ string of this object
+ */
+ private void cleanMRZString() {
+ try {
+ String[] spl = mrz.replace(" ", "").replace("\n\n", "\n").split("\n"); // Delete any space characters and replace double newline with a single newline
+ mrz = spl[0] + "\n" + spl[1]; // Extract only first 2 lines, sometimes random errorous data is detected beyond.
+ }catch (Exception e) {
+ }
+ }
+
+ /**
+ * Performs checksum calculation of the given string's chars from start til end.
+ * Uses value at index {@code checkIndex} in {@code string} as check value.
+ * @param string String to be checked
+ * @param ranges indices of substrings to be checked
+ * @param checkIndex index of char to check against
+ * @return boolean whether check was successful
+ */
+ private static boolean checkSum (String string, int[][] ranges, int checkIndex) {
+ int[] code = { 7, 3, 1};
+ int checkValue = Character.getNumericValue(string.charAt(checkIndex));
+ int count = 0;
+ float checkSum = 0;
+ for (int[] range : ranges) {
+ char[] line = string.substring(range[0], range[1]).toCharArray();
+ for (int i = 0; i < line.length; i++) {
+ int num = 0;
+ if (Character.toString(line[i]).matches("[A-Z]")) {
+ num = ((int) line[i] - 55);
+ } else if (Character.toString(line[i]).matches("\\d")) {
+ num = Character.getNumericValue(line[i]);
+ } else if (Character.toString(line[i]).matches("<")) {
+ num = 0;
+ } else {
+ return false; //illegal character
+ }
+ checkSum += num * code[count%3];
+ count++;
+ }
+ }
+ int rem = (int) checkSum % 10;
+ return rem == checkValue;
+ }
+
+ /**
+ * Returns relevant data from the MRZ in a hashmap.
+ * @return hashmap
+ */
+ public HashMap getPrettyData() {
+ HashMap data = new HashMap<>();
+ if (mrz.startsWith("P")) {
+ data.put(MainActivity.DOCUMENT_NUMBER, mrz.split("\n")[1].substring(PASSPORT_DOCNO_INDICES[0], PASSPORT_DOCNO_INDICES[1]));
+ data.put(MainActivity.DATE_OF_BIRTH, mrz.split("\n")[1].substring(PASSPORT_DOB_INDICES[0], PASSPORT_DOB_INDICES[1]));
+ data.put(MainActivity.EXPIRATION_DATE, mrz.split("\n")[1].substring(PASSPORT_EXP_INDICES[0],PASSPORT_EXP_INDICES[1]));
+ } else if (mrz.startsWith("I")) {
+ data.put(MainActivity.DOCUMENT_NUMBER, mrz.split("\n")[0].substring(ID_DOCNO_INDICES[0],ID_DOCNO_INDICES[1]));
+ data.put(MainActivity.DATE_OF_BIRTH, mrz.split("\n")[1].substring(ID_DOB_INDICES[0],ID_DOB_INDICES[1]));
+ data.put(MainActivity.EXPIRATION_DATE, mrz.split("\n")[1].substring(ID_EXP_INDICES[0],ID_EXP_INDICES[1]));
+ }
+ return data;
+ }
+
+ /**
+ * Checks if this MRZ data is valid
+ * @return boolean whether the given input is a correct MRZ.
+ */
+ public boolean valid() {
+ try {
+ if (mrz.startsWith("P")) {
+ return checkPassportMRZ(mrz);
+ } else if (mrz.startsWith("I")){
+ return checkIDMRZ(mrz);
+ }
+ } catch (Exception e) {
+ // Probably an outOfBounds indicating the format was incorrect
+ }
+ return false;
+ }
+
+ private boolean checkIDMRZ(String mrz) {
+ boolean firstCheck = checkSum(mrz.split("\n")[0], new int[][]{ID_DOCNO_INDICES}, 14); //Checks document number
+ boolean secondCheck = checkSum(mrz.split("\n")[1], new int[][]{ID_DOB_INDICES}, 6); //Checks DoB
+ boolean thirdCheck = checkSum(mrz.split("\n")[1], new int[][]{ID_EXP_INDICES}, 14); //Checks expiration date
+ boolean fourthCheck = checkSum(mrz.replace("\n", ""), new int[][]{{5, 30}, {30, 37}, {38, 45}, {49, 59}}, 59); //Checks upper line from 6th digit + middle line
+ return firstCheck && secondCheck && thirdCheck && fourthCheck;
+ }
+
+ private boolean checkPassportMRZ(String mrz) {
+ boolean firstCheck = checkSum(mrz.split("\n")[1], new int[][]{PASSPORT_DOCNO_INDICES}, 9); // Checks document number
+ boolean secondCheck = checkSum(mrz.split("\n")[1], new int[][]{PASSPORT_DOB_INDICES}, 19);
+ boolean thirdCheck = checkSum(mrz.split("\n")[1], new int[][]{PASSPORT_EXP_INDICES}, 27);
+ boolean fourthCheck = true;//may be checkSum(mrz.split("\n")[1], new int[][]{{13, 19}}, 19);
+ boolean fifthCheck = checkSum(mrz.split("\n")[1], new int[][]{{0, 10}, {13, 20}, {21, 43}}, 43);
+ return firstCheck && secondCheck && thirdCheck && fourthCheck && fifthCheck;
+ }
+
+ public String getText() {
+ return mrz;
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/ocrscanner/TesseractOCR.java b/app/src/main/java/com/digitalvotingpass/ocrscanner/TesseractOCR.java
new file mode 100644
index 0000000..9513f6b
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/ocrscanner/TesseractOCR.java
@@ -0,0 +1,228 @@
+package com.digitalvotingpass.ocrscanner;
+
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import com.digitalvotingpass.camera.Camera2BasicFragment;
+import com.digitalvotingpass.utilities.Util;
+import com.googlecode.tesseract.android.TessBaseAPI;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.Semaphore;
+
+public class TesseractOCR {
+ private static final String TAG = "TesseractOCR";
+
+ private static final long INTER_SCAN_DELAY_MILLIS = 500;
+ private static final long OCR_SCAN_TIMEOUT_MILLIS = 5000;
+
+ private static final String trainedData = "eng.traineddata";
+ private final String name;
+
+ private TessBaseAPI baseApi;
+ private HandlerThread myThread;
+ private Handler myHandler;
+ private Handler cleanHandler;
+ private Handler timeoutHandler;
+
+ private AssetManager assetManager;
+ private Camera2BasicFragment fragment;
+ private boolean stopping = false;
+ private boolean isInitialized = false;
+
+ // Filled with OCR run times for analysis
+ private ArrayList times = new ArrayList<>();
+
+ /**
+ * Lock to ensure only one thread can start copying to device storage.
+ */
+ private static Semaphore mDeviceStorageAccessLock = new Semaphore(1);
+
+ /**
+ * CURRENTLY NOT FUNCTIONAL
+ * Timeout Thread, should end OCR detection when timeout occurs
+ */
+ private Runnable timeout = new Runnable() {
+ @Override
+ public void run() {
+ Log.e(TAG, "TIMEOUT");
+// baseApi.stop(); // Does not stop baseApi.getUTF8Text()
+ }
+ };
+
+
+ private Runnable scan = new Runnable() {
+ @Override
+ public void run() {
+ while (!stopping) {
+ Log.v(TAG, "Start Scan");
+ timeoutHandler.postDelayed(timeout, OCR_SCAN_TIMEOUT_MILLIS);
+ long time = System.currentTimeMillis();
+ Bitmap b = fragment.extractBitmap();
+ Mrz mrz = ocr(b);
+ long timetook = System.currentTimeMillis() - time;
+ Log.i(TAG, "took " + timetook / 1000f + " sec");
+ times.add(timetook);
+ if (mrz != null && mrz.valid()) {
+ fragment.scanResultFound(mrz);
+ }
+ timeoutHandler.removeCallbacks(timeout);
+ try {
+ Thread.sleep(INTER_SCAN_DELAY_MILLIS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ Log.e(TAG, "Stopping scan");
+ }
+ };
+
+ public TesseractOCR(String name, Camera2BasicFragment fragment, final AssetManager assetManager) {
+ this.assetManager = assetManager;
+ this.fragment = fragment;
+ this.name = name;
+ }
+
+ /**
+ * Starts OCR scan routine with delay in msec
+ * @param delay int how msec before start
+ */
+ public void startScanner(int delay) {
+ myHandler.postDelayed(scan, delay);
+ }
+
+ /**
+ * Starts (enqueues) a stop routine in a new thread, then returns immediately.
+ */
+ public void stopScanner() {
+ cleanHandler = new Handler();
+ cleanHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cleanup();
+ }
+ });
+ }
+
+ /**
+ * Starts a new thread to do OCR and enqueues an initialization task;
+ */
+ public void initialize() {
+ myThread = new HandlerThread(name);
+ myThread.start();
+ timeoutHandler = new Handler();
+ myHandler = new Handler(myThread.getLooper());
+ myHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ init();
+ Log.e(TAG, "INIT DONE");
+ }
+ });
+ isInitialized = true;
+ }
+
+ /**
+ * Initializes Tesseract library using traineddata file.
+ */
+ private void init() {
+ baseApi = new TessBaseAPI();
+ baseApi.setDebug(true);
+ String path = Environment.getExternalStorageDirectory() + "/";
+ File trainedDataFile = new File(Environment.getExternalStorageDirectory(), "/tessdata/" + trainedData);
+ try {
+ mDeviceStorageAccessLock.acquire();
+ if (!trainedDataFile.exists()) {
+ Log.i(TAG, "No existing trained data found, copying from assets..");
+ Util.copyAssetsFile(assetManager.open(trainedData), trainedDataFile);
+ } else {
+ Log.i(TAG, "Existing trained data found");
+ }
+ mDeviceStorageAccessLock.release();
+ baseApi.init(path, trainedData.replace(".traineddata", "")); //extract language code from trained data file
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ //TODO show error to user, coping failed
+ }
+ }
+
+ /**
+ * Performs OCR scan to bitmap provided, if tesseract is initialized and not currently stopping.
+ * @param bitmap Bitmap image to be scanned
+ * @return Mrz Object containing result data
+ */
+ private Mrz ocr(Bitmap bitmap) {
+ if (bitmap == null) return null;
+ if (isInitialized && !stopping) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 2;
+ Log.v(TAG, "Image dims x: " + bitmap.getWidth() + ", y: " + bitmap.getHeight());
+ baseApi.setImage(bitmap);
+ baseApi.setVariable("tessedit_char_whitelist",
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ<");
+ baseApi.setVariable("max_permuter_attempts", "20");//Sets the max no. of tries TODO try some more values
+// baseApi.setVariable("load_freq_dawg", "0");
+// baseApi.setVariable("load_system_dawg", "0");
+// baseApi.setVariable("load_punc_dawg", "0");
+ baseApi.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO);
+// baseApi.setVariable(TessBaseAPI.OEM_TESSERACT_ONLY, "1");
+// String s = baseApi.getHOCRText(0);
+ String recognizedText = baseApi.getUTF8Text();
+// recognizedText = recognizedText.replaceAll("<.*?>","");
+// recognizedText = recognizedText.replaceAll("<", "<");
+// recognizedText = recognizedText.replaceAll("^$", "");
+ Log.v(TAG, "OCR Result: " + recognizedText);
+ return new Mrz(recognizedText);
+ } else {
+ Log.e(TAG, "Trying ocr() while not initalized or stopping!");
+ return null;
+ }
+ }
+
+ /**
+ * Cleans memory used by Tesseract library and closes OCR thread.
+ * After this has been called initialize() needs to be called to restart the thread and init Tesseract
+ */
+ public void cleanup () {
+ if (isInitialized) {
+ giveStats();
+ stopping = true;
+ myThread.quitSafely();
+ myHandler.removeCallbacks(scan);
+ timeoutHandler.removeCallbacks(timeout);
+ try {
+ myThread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ myThread = null;
+ myHandler = null;
+ baseApi.end();
+ isInitialized = false;
+ stopping = false;
+ }
+ }
+
+ /**
+ * Prints some statistics about the run time of the OCR scanning
+ */
+ private void giveStats() {
+ long max = 0;
+ long curravg = 0;
+ for (int i=0; i < times.size(); i++) {
+ if (times.get(i) > max) max = times.get(i);
+ curravg += times.get(i);
+ }
+ // prevent divide by zero
+ if(times.size()>0) {
+ Log.e(TAG, "Max runtime was " + max / 1000f + " sec and avg was " + curravg / times.size() / 1000f + " tot tries: " + times.size());
+ }
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/passportconnection/PassportConActivity.java b/app/src/main/java/com/digitalvotingpass/passportconnection/PassportConActivity.java
new file mode 100644
index 0000000..b43f2bc
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/passportconnection/PassportConActivity.java
@@ -0,0 +1,192 @@
+package com.digitalvotingpass.passportconnection;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.digitalvotingpass.digitalvotingpass.R;
+import com.digitalvotingpass.digitalvotingpass.ResultActivity;
+
+import org.jmrtd.PassportService;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+
+import java.security.PublicKey;
+import java.security.Security;
+import java.util.HashMap;
+
+public class PassportConActivity extends AppCompatActivity {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ // Adapter for NFC connection
+ private NfcAdapter mNfcAdapter;
+ private HashMap documentData;
+ private ImageView progressView;
+
+ /**
+ * This activity usually be loaded from the starting screen of the app.
+ * This method handles the start-up of the activity, it does not need to call any other methods
+ * since the activity onNewIntent() calls the intentHandler when a NFC chip is detected.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Bundle extras = getIntent().getExtras();
+ documentData = (HashMap) extras.get("docData");
+
+ setContentView(R.layout.activity_passport_con);
+ Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
+ setSupportActionBar(appBar);
+ TextView notice = (TextView) findViewById(R.id.notice);
+ progressView = (ImageView) findViewById(R.id.progress_view);
+
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ if (mNfcAdapter == null) {
+ // Stop here, we definitely need NFC
+ Toast.makeText(this, R.string.nfc_not_supported_error, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+ if (!mNfcAdapter.isEnabled()) {
+ notice.setText(R.string.nfc_disabled_error);
+ } else {
+ notice.setText(R.string.nfc_enabled);
+ }
+ }
+
+ /**
+ * Some methods to ensure that when the activity is opened the ID is read.
+ * When the activity is opened any nfc device held against the phone will cause, handleIntent to
+ * be called.
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // It's important, that the activity is in the foreground (resumed). Otherwise an IllegalStateException is thrown.
+ setupForegroundDispatch(this, mNfcAdapter);
+ }
+
+ @Override
+ protected void onPause() {
+ // Call this before super.onPause, otherwise an IllegalArgumentException is thrown as well.
+ stopForegroundDispatch(this, mNfcAdapter);
+
+ super.onPause();
+ }
+
+ /**
+ * This method gets called, when a new Intent gets associated with the current activity instance.
+ * Instead of creating a new activity, onNewIntent will be called. For more information have a look
+ * at the documentation.
+ *
+ * In our case this method gets called, when the user attaches a Tag to the device.
+ */
+ @Override
+ protected void onNewIntent(Intent intent) {
+ handleIntent(intent);
+ }
+
+ /**
+ * Setup the recognition of nfc tags when the activity is opened (foreground)
+ *
+ * @param activity The corresponding {@link Activity} requesting the foreground dispatch.
+ * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
+ */
+ public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) {
+ final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass());
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+
+ final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0);
+
+ IntentFilter[] filters = new IntentFilter[1];
+ String[][] techList = new String[][]{};
+
+ // Filter for nfc tag discovery
+ filters[0] = new IntentFilter();
+ filters[0].addAction(NfcAdapter.ACTION_TAG_DISCOVERED);
+ filters[0].addCategory(Intent.CATEGORY_DEFAULT);
+
+ adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList);
+ }
+
+ /**
+ * @param activity The corresponding {@link BaseActivity} requesting to stop the foreground dispatch.
+ * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
+ */
+ public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) {
+ adapter.disableForegroundDispatch(activity);
+ }
+
+ /**
+ * Handle the intent following from a NFC detection.
+ *
+ */
+ private void handleIntent(Intent intent) {
+ progressView.setImageResource(R.drawable.nfc_icon_1);
+
+ // if nfc tag holds no data, return
+ Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ if (tag == null) {
+ return;
+ }
+
+ // Open a connection with the ID, return a PassportService object which holds the open connection
+ PassportConnection pcon= new PassportConnection();
+ PassportService ps = pcon.openConnection(tag, documentData);
+ try {
+ progressView.setImageResource(R.drawable.nfc_icon_2);
+
+
+ // display data from dg15
+ PublicKey pubKey = pcon.getAAPublicKey(ps);
+
+ // sign 8 bytes of data and display the signed data + length
+ byte[] signedData = pcon.signData(ps);
+ progressView.setImageResource(R.drawable.nfc_icon_3);
+
+ // when all data is loaded start ResultActivity
+ startResultActivity(pubKey, signedData);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ Toast.makeText(this, R.string.NFC_error, Toast.LENGTH_LONG).show();
+ progressView.setImageResource(R.drawable.nfc_icon_empty);
+ } finally {
+ try {
+ ps.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Method to start the ResultActivity once all the data is loaded.
+ * Creates new intent with the read data
+ * @param pubKey
+ * @param signedData
+ */
+ public void startResultActivity(PublicKey pubKey, byte[] signedData) {
+ if(pubKey != null && signedData != null) {
+
+ Intent intent = new Intent(getApplicationContext(), ResultActivity.class);
+ intent.putExtra("pubKey", pubKey);
+ intent.putExtra("signedData", signedData);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, R.string.NFC_error, Toast.LENGTH_LONG).show();
+ progressView.setImageResource(R.drawable.nfc_icon_empty);
+ }
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/passportconnection/PassportConnection.java b/app/src/main/java/com/digitalvotingpass/passportconnection/PassportConnection.java
new file mode 100644
index 0000000..98433eb
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/passportconnection/PassportConnection.java
@@ -0,0 +1,132 @@
+package com.digitalvotingpass.passportconnection;
+
+import android.nfc.Tag;
+import android.nfc.tech.IsoDep;
+
+import com.digitalvotingpass.digitalvotingpass.MainActivity;
+import com.digitalvotingpass.utilities.Util;
+
+import net.sf.scuba.smartcards.CardService;
+
+import org.jmrtd.BACKeySpec;
+import org.jmrtd.PassportService;
+import org.jmrtd.lds.DG15File;
+import org.jmrtd.lds.DG1File;
+import org.jmrtd.lds.LDSFileUtil;
+
+import java.io.InputStream;
+import java.security.PublicKey;
+import java.util.HashMap;
+
+public class PassportConnection {
+
+ /**
+ * Opens a connection with the ID by doing BAC
+ * Uses hardcoded parameters for now
+ *
+ * @param tag - NFC tag that started this activity (ID NFC tag)
+ * @return PassportService - passportservice that has an open connection with the ID
+ */
+ public PassportService openConnection(Tag tag, final HashMap docData) {
+ PassportService ps = null;
+ try {
+ IsoDep nfc = IsoDep.get(tag);
+ CardService cs = CardService.getInstance(nfc);
+ ps = new PassportService(cs);
+ ps.open();
+
+ // Get the information needed for BAC from the data provided by OCR
+ ps.sendSelectApplet(false);
+ BACKeySpec bacKey = new BACKeySpec() {
+ @Override
+ public String getDocumentNumber() {
+ return docData.get(MainActivity.DOCUMENT_NUMBER);
+ }
+
+ @Override
+ public String getDateOfBirth() { return docData.get(MainActivity.DATE_OF_BIRTH); }
+
+ @Override
+ public String getDateOfExpiry() { return docData.get(MainActivity.EXPIRATION_DATE); }
+ };
+
+ ps.doBAC(bacKey);
+ return ps;
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ ps.close();
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the public key used for Active Authentication from datagroup 15.
+ *
+ * @return Publickey - returns the publickey used for AA
+ */
+ public PublicKey getAAPublicKey(PassportService ps) {
+ InputStream is15 = null;
+ try {
+ is15 = ps.getInputStream(PassportService.EF_DG15);
+ DG15File dg15 = new DG15File(is15);
+ return dg15.getPublicKey();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ } finally {
+ try {
+ is15.close();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Signs 8 bytes by the passport using the AA functionality.
+ *
+ * @return byte[] - signed byte array
+ */
+ public byte[] signData(PassportService ps) {
+ InputStream is15 = null;
+ try {
+ is15 = ps.getInputStream(PassportService.EF_DG15);
+ // test 8 byte string for testing purposes
+ byte[] data = Util.hexStringToByteArray("0a1b3c4d5e6faabb");
+ // doAA of JMRTD library only returns signed data, and does not have the AA functionality yet
+ // there is no need for sending public key information with the method.
+ return ps.doAA(null, null, null, data);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ } finally {
+ try {
+ is15.close();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the BSN from datagroup1 to confirm the ID was scanned correctly
+ * This is for testing purposes
+ */
+ public String getBSN(PassportService ps) {
+ InputStream is = null;
+ try {
+ is = ps.getInputStream(PassportService.EF_DG1);
+ DG1File dg1 = (DG1File) LDSFileUtil.getLDSFile(PassportService.EF_DG1, is);
+ return dg1.getMRZInfo().getPersonalNumber();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ } finally {
+ try {
+ is.close();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/transactionhistory/Transaction.java b/app/src/main/java/com/digitalvotingpass/transactionhistory/Transaction.java
new file mode 100644
index 0000000..00c6d11
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/transactionhistory/Transaction.java
@@ -0,0 +1,19 @@
+package com.digitalvotingpass.transactionhistory;
+
+import java.util.Date;
+
+/**
+ * Class used to store the transactions that are displayed in the transaction history activity.
+ * TODO: refactor this to make use of actual details of the transactions
+ */
+public class Transaction {
+ public String title;
+ public Date time;
+ public String transactionDetails;
+
+ public Transaction(String title, Date time, String transactionDetails) {
+ this.title = title;
+ this.time = time;
+ this.transactionDetails = transactionDetails;
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/transactionhistory/TransactionHistoryActivity.java b/app/src/main/java/com/digitalvotingpass/transactionhistory/TransactionHistoryActivity.java
new file mode 100644
index 0000000..10f5a2d
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/transactionhistory/TransactionHistoryActivity.java
@@ -0,0 +1,39 @@
+package com.digitalvotingpass.transactionhistory;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.support.v7.widget.Toolbar;
+import android.widget.ListView;
+
+import com.digitalvotingpass.digitalvotingpass.R;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+public class TransactionHistoryActivity extends AppCompatActivity {
+ private ListView transactionList;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_transaction_history);
+ Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
+ setSupportActionBar(appBar);
+
+ transactionList = (ListView) findViewById(R.id.transaction_list);
+
+ // create a transaction history array with all the transactions and add them to the list
+ // TODO: handle actual transaction history input data
+ ArrayList transactionHistory = new ArrayList<>();
+
+ TransactionsAdapter adapter = new TransactionsAdapter(this, transactionHistory);
+ transactionList.setAdapter(adapter);
+
+ Transaction newTransaction = new Transaction("Received voting pass", new Date(), "some details about the transaction");
+ Transaction newTransaction2 = new Transaction("Cast vote", new Date(), "some details about the transaction");
+
+ adapter.add(newTransaction);
+ adapter.add(newTransaction2);
+
+ }
+}
diff --git a/app/src/main/java/com/digitalvotingpass/transactionhistory/TransactionsAdapter.java b/app/src/main/java/com/digitalvotingpass/transactionhistory/TransactionsAdapter.java
new file mode 100644
index 0000000..82b8242
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/transactionhistory/TransactionsAdapter.java
@@ -0,0 +1,47 @@
+package com.digitalvotingpass.transactionhistory;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import com.digitalvotingpass.digitalvotingpass.R;
+
+import java.util.ArrayList;
+
+/**
+ * Created by wkmeijer on 26-5-17.
+ */
+
+public class TransactionsAdapter extends ArrayAdapter {
+ public TransactionsAdapter(Context context, ArrayList transactions) {
+ super(context, 0, transactions);
+ }
+
+ /**
+ * Handle the conversion from the transaction object to the list item textviews.
+ * TODO: handle actual transaction data
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Get the data item for this position
+ Transaction transaction = getItem(position);
+ // Check if an existing view is being reused, otherwise inflate the view
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_transaction, parent, false);
+ }
+ // Lookup view for data population
+ TextView title = (TextView) convertView.findViewById(R.id.title);
+ TextView time = (TextView) convertView.findViewById(R.id.time);
+ TextView details = (TextView) convertView.findViewById(R.id.details);
+ // Populate the data into the template view using the data object
+ title.setText(transaction.title);
+ time.setText(transaction.time.toString());
+ details.setText(transaction.transactionDetails);
+
+ // Return the completed view to render on screen
+ return convertView;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/digitalvotingpass/utilities/Util.java b/app/src/main/java/com/digitalvotingpass/utilities/Util.java
new file mode 100644
index 0000000..71a29ca
--- /dev/null
+++ b/app/src/main/java/com/digitalvotingpass/utilities/Util.java
@@ -0,0 +1,81 @@
+package com.digitalvotingpass.utilities;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Util {
+
+ /**
+ * Copies an InputStream into a File.
+ * This is used to copy an InputStream from the assets folder to a file in the FileSystem.
+ * Creates nay non-existant parent folders to f.
+ * @param is InputStream to be copied.
+ * @param f File to copy data to.
+ */
+ public static void copyAssetsFile(InputStream is, File f) throws IOException {
+ OutputStream os = null;
+ if (!f.exists()) {
+ if (!f.getParentFile().mkdirs()) { //getParent because otherwise it creates a folder with that filename, we just need the dirs
+ Log.e("Util", "Cannot create path!");
+ }
+ }
+ os = new FileOutputStream(f, true);
+
+ final int buffer_size = 1024 * 1024;
+ try {
+ byte[] bytes = new byte[buffer_size];
+ for (;;)
+ {
+ int count = is.read(bytes, 0, buffer_size);
+ if (count == -1)
+ break;
+ os.write(bytes, 0, count);
+ }
+ is.close();
+ os.close();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ /**
+ * Method for converting a hexString to a byte array.
+ * This method is used for signing transaction hashes (which are in hex).
+ */
+ public static byte[] hexStringToByteArray(String hStr) {
+ if(hStr != null) {
+ int len = hStr.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(hStr.charAt(i), 16) << 4)
+ + Character.digit(hStr.charAt(i + 1), 16));
+ }
+ return data;
+ }
+ return new byte[0];
+ }
+
+ /**
+ * Method for converting a byte array to a hexString.
+ * This method is used for converting a signed 8-byte array back to a hashString in order to
+ * display it readable.
+ */
+ public static String byteArrayToHexString(byte[] bArray) {
+ if (bArray != null) {
+ final char[] hexArray = "0123456789ABCDEF".toCharArray();
+ char[] hexChars = new char[bArray.length * 2];
+ for (int j = 0; j < bArray.length; j++) {
+ int v = bArray[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+ return "";
+ }
+}
diff --git a/app/src/main/res/drawable-mdpi/approve.png b/app/src/main/res/drawable-mdpi/approve.png
new file mode 100644
index 0000000..bec353a
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/approve.png differ
diff --git a/app/src/main/res/drawable-mdpi/cancel_icon.png b/app/src/main/res/drawable-mdpi/cancel_icon.png
new file mode 100644
index 0000000..0e2b1e0
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cancel_icon.png differ
diff --git a/app/src/main/res/drawable-mdpi/manual_input_icon.png b/app/src/main/res/drawable-mdpi/manual_input_icon.png
new file mode 100644
index 0000000..d3599c5
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/manual_input_icon.png differ
diff --git a/app/src/main/res/drawable-mdpi/nfc_icon_1.png b/app/src/main/res/drawable-mdpi/nfc_icon_1.png
new file mode 100644
index 0000000..3523b74
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/nfc_icon_1.png differ
diff --git a/app/src/main/res/drawable-mdpi/nfc_icon_2.png b/app/src/main/res/drawable-mdpi/nfc_icon_2.png
new file mode 100644
index 0000000..97ac02c
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/nfc_icon_2.png differ
diff --git a/app/src/main/res/drawable-mdpi/nfc_icon_3.png b/app/src/main/res/drawable-mdpi/nfc_icon_3.png
new file mode 100644
index 0000000..6ad0297
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/nfc_icon_3.png differ
diff --git a/app/src/main/res/drawable-mdpi/nfc_icon_empty.png b/app/src/main/res/drawable-mdpi/nfc_icon_empty.png
new file mode 100644
index 0000000..9c37829
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/nfc_icon_empty.png differ
diff --git a/app/src/main/res/drawable-mdpi/not_approve.png b/app/src/main/res/drawable-mdpi/not_approve.png
new file mode 100644
index 0000000..d3852f8
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/not_approve.png differ
diff --git a/app/src/main/res/drawable-mdpi/scan_passport_icon.png b/app/src/main/res/drawable-mdpi/scan_passport_icon.png
new file mode 100644
index 0000000..442d2e1
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/scan_passport_icon.png differ
diff --git a/app/src/main/res/drawable-mdpi/voting_pass_icon.png b/app/src/main/res/drawable-mdpi/voting_pass_icon.png
new file mode 100644
index 0000000..334362b
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/voting_pass_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/approve.png b/app/src/main/res/drawable-xhdpi/approve.png
new file mode 100644
index 0000000..a606f41
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/approve.png differ
diff --git a/app/src/main/res/drawable-xhdpi/cancel_icon.png b/app/src/main/res/drawable-xhdpi/cancel_icon.png
new file mode 100644
index 0000000..ce8be53
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cancel_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/manual_input_icon.png b/app/src/main/res/drawable-xhdpi/manual_input_icon.png
new file mode 100644
index 0000000..ca047e6
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/manual_input_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/nfc_icon_1.png b/app/src/main/res/drawable-xhdpi/nfc_icon_1.png
new file mode 100644
index 0000000..f4f3af5
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nfc_icon_1.png differ
diff --git a/app/src/main/res/drawable-xhdpi/nfc_icon_2.png b/app/src/main/res/drawable-xhdpi/nfc_icon_2.png
new file mode 100644
index 0000000..4c9c330
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nfc_icon_2.png differ
diff --git a/app/src/main/res/drawable-xhdpi/nfc_icon_3.png b/app/src/main/res/drawable-xhdpi/nfc_icon_3.png
new file mode 100644
index 0000000..0648b2b
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nfc_icon_3.png differ
diff --git a/app/src/main/res/drawable-xhdpi/nfc_icon_empty.png b/app/src/main/res/drawable-xhdpi/nfc_icon_empty.png
new file mode 100644
index 0000000..15c950f
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nfc_icon_empty.png differ
diff --git a/app/src/main/res/drawable-xhdpi/not_approve.png b/app/src/main/res/drawable-xhdpi/not_approve.png
new file mode 100644
index 0000000..47458df
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/not_approve.png differ
diff --git a/app/src/main/res/drawable-xhdpi/scan_passport_icon.png b/app/src/main/res/drawable-xhdpi/scan_passport_icon.png
new file mode 100644
index 0000000..5a260d1
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/scan_passport_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/voting_pass_icon.png b/app/src/main/res/drawable-xhdpi/voting_pass_icon.png
new file mode 100644
index 0000000..272de25
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voting_pass_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/approve.png b/app/src/main/res/drawable-xxhdpi/approve.png
new file mode 100644
index 0000000..2bf7656
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/approve.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/cancel_icon.png b/app/src/main/res/drawable-xxhdpi/cancel_icon.png
new file mode 100644
index 0000000..b6ab76f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cancel_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/manual_input_icon.png b/app/src/main/res/drawable-xxhdpi/manual_input_icon.png
new file mode 100644
index 0000000..629295b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/manual_input_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/nfc_icon_1.png b/app/src/main/res/drawable-xxhdpi/nfc_icon_1.png
new file mode 100644
index 0000000..ac21e8f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/nfc_icon_1.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/nfc_icon_2.png b/app/src/main/res/drawable-xxhdpi/nfc_icon_2.png
new file mode 100644
index 0000000..596a3e4
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/nfc_icon_2.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/nfc_icon_3.png b/app/src/main/res/drawable-xxhdpi/nfc_icon_3.png
new file mode 100644
index 0000000..e1acdcd
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/nfc_icon_3.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/nfc_icon_empty.png b/app/src/main/res/drawable-xxhdpi/nfc_icon_empty.png
new file mode 100644
index 0000000..0ce046e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/nfc_icon_empty.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/not_approve.png b/app/src/main/res/drawable-xxhdpi/not_approve.png
new file mode 100644
index 0000000..accb455
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/not_approve.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/scan_passport_icon.png b/app/src/main/res/drawable-xxhdpi/scan_passport_icon.png
new file mode 100644
index 0000000..0b1a245
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/scan_passport_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/voting_pass_icon.png b/app/src/main/res/drawable-xxhdpi/voting_pass_icon.png
new file mode 100644
index 0000000..4715957
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/voting_pass_icon.png differ
diff --git a/app/src/main/res/drawable/blue_button.xml b/app/src/main/res/drawable/blue_button.xml
new file mode 100644
index 0000000..626d484
--- /dev/null
+++ b/app/src/main/res/drawable/blue_button.xml
@@ -0,0 +1,24 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/blue_button_bottom.xml b/app/src/main/res/drawable/blue_button_bottom.xml
new file mode 100644
index 0000000..8826106
--- /dev/null
+++ b/app/src/main/res/drawable/blue_button_bottom.xml
@@ -0,0 +1,30 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/card_button.xml b/app/src/main/res/drawable/card_button.xml
new file mode 100644
index 0000000..de2bfad
--- /dev/null
+++ b/app/src/main/res/drawable/card_button.xml
@@ -0,0 +1,24 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/grey_border.xml b/app/src/main/res/drawable/grey_border.xml
new file mode 100644
index 0000000..5b1f0c0
--- /dev/null
+++ b/app/src/main/res/drawable/grey_border.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/white_border.xml b/app/src/main/res/drawable/white_border.xml
new file mode 100644
index 0000000..5f2175d
--- /dev/null
+++ b/app/src/main/res/drawable/white_border.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml
new file mode 100644
index 0000000..86ec116
--- /dev/null
+++ b/app/src/main/res/layout/activity_camera.xml
@@ -0,0 +1,22 @@
+
+
diff --git a/app/src/main/res/layout/activity_election_choice.xml b/app/src/main/res/layout/activity_election_choice.xml
new file mode 100644
index 0000000..a6acf35
--- /dev/null
+++ b/app/src/main/res/layout/activity_election_choice.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..fc19100
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_manual_input.xml b/app/src/main/res/layout/activity_manual_input.xml
new file mode 100644
index 0000000..91efc3e
--- /dev/null
+++ b/app/src/main/res/layout/activity_manual_input.xml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_passport_con.xml b/app/src/main/res/layout/activity_passport_con.xml
new file mode 100644
index 0000000..d3a69b9
--- /dev/null
+++ b/app/src/main/res/layout/activity_passport_con.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_result.xml b/app/src/main/res/layout/activity_result.xml
new file mode 100644
index 0000000..c103c3b
--- /dev/null
+++ b/app/src/main/res/layout/activity_result.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_splash_screen.xml b/app/src/main/res/layout/activity_splash_screen.xml
new file mode 100644
index 0000000..179a53a
--- /dev/null
+++ b/app/src/main/res/layout/activity_splash_screen.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_transaction_history.xml b/app/src/main/res/layout/activity_transaction_history.xml
new file mode 100644
index 0000000..bc4b63a
--- /dev/null
+++ b/app/src/main/res/layout/activity_transaction_history.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_camera2_basic.xml b/app/src/main/res/layout/fragment_camera2_basic.xml
new file mode 100644
index 0000000..4a8c1b7
--- /dev/null
+++ b/app/src/main/res/layout/fragment_camera2_basic.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_election.xml b/app/src/main/res/layout/item_election.xml
new file mode 100644
index 0000000..8a8fbd4
--- /dev/null
+++ b/app/src/main/res/layout/item_election.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_transaction.xml b/app/src/main/res/layout/item_transaction.xml
new file mode 100644
index 0000000..b694bb1
--- /dev/null
+++ b/app/src/main/res/layout/item_transaction.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/spinner_dropdown.xml b/app/src/main/res/layout/spinner_dropdown.xml
new file mode 100644
index 0000000..e47c810
--- /dev/null
+++ b/app/src/main/res/layout/spinner_dropdown.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/choice_menu.xml b/app/src/main/res/menu/choice_menu.xml
new file mode 100644
index 0000000..85823f4
--- /dev/null
+++ b/app/src/main/res/menu/choice_menu.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/result_menu.xml b/app/src/main/res/menu/result_menu.xml
new file mode 100644
index 0000000..7e4ac8b
--- /dev/null
+++ b/app/src/main/res/menu/result_menu.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..19f6066
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..cfb9b63
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..ca76852
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..289ed1e
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..25389f9
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
new file mode 100644
index 0000000..e9da9be
--- /dev/null
+++ b/app/src/main/res/values-land/dimens.xml
@@ -0,0 +1,4 @@
+
+
+ 80dp
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6bc82bd
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,26 @@
+
+
+ #3f51b5
+ #303F9F
+ #ff4081
+
+ #a0ffffff
+ #cc4285f4
+ #a0ffffff
+
+ #a0f88938
+ #a0acacac
+ #00ff00
+ #ffffff
+
+ #01689b
+ #01517a
+ #015f8e
+ #cce0f1
+ #01689b
+ #f3f3f3
+
+ #d52b1e
+ #b08800
+ #328a3b
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..8b4a870
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+
+ 150dp
+ 15dp
+ 16dp
+ 190dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..13b8af0
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,76 @@
+
+ Digital Voting Pass
+
+
+ Manual input
+ Submit
+ Manual input document details
+ Submit
+ Document number
+ Expiry date
+ Date of birth
+ XXP100154
+ yymmdd
+ yymmdd
+ or
+ Input of document number is not correct, please recheck your input.
+ Please fill in the expiry date of your document in the yymmdd format.
+ Please fill in your date of birth in the yymmdd format.
+ Please fill in the document number.
+ Please fill in the expiry date of the document.
+ Please fill in your date of birth.
+ Info
+ This sample needs camera permission.
+ This device doesn\'t support Camera2 API.
+
+
+ Manual Input
+ This app needs camera permission.
+ This device doesn\'t support Camera2 API.
+ This App needs storage permissions in order to use OCR.
+ Scan the text at the bottom of the passport or on the backside of the ID card.
+
+
+ This device doesn\'t support NFC.
+ NFC is disabled, please enable NFC and hold your ID to the back of the phone to start scanning.
+ Please hold your ID to the back of your phone.
+ Oops, something went wrong, please check the terminal output.
+ Could not complete NFC scan, please try again.
+ Start ID connection
+ Please scan the ID first
+ Transaction history
+
+ Scan passport
+ Authorization
+
+ Next voter
+ Confirm vote
+ Cancel vote
+
+ Authorization failed, do not hand out voting ballot.
+ Authorization succesful, hand out voting ballot.
+ Waiting for authorization.
+
+ %s has right to:
+ Voting pass
+ Voting passes
+
+ Search
+ Choose election
+ election
+
+
+ - January
+ - February
+ - March
+ - April
+ - May
+ - June
+ - July
+ - August
+ - September
+ - October
+ - November
+ - December
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d8447a3
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml
new file mode 100644
index 0000000..7b0fce2
--- /dev/null
+++ b/app/src/main/res/xml/searchable.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/digitalvotingpass/ocrscanner/MrzTest.java b/app/src/test/java/com/digitalvotingpass/ocrscanner/MrzTest.java
new file mode 100644
index 0000000..bf8743b
--- /dev/null
+++ b/app/src/test/java/com/digitalvotingpass/ocrscanner/MrzTest.java
@@ -0,0 +1,84 @@
+package com.digitalvotingpass.ocrscanner;
+
+import com.digitalvotingpass.ocrscanner.Mrz;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Created by jonathan on 5/16/17.
+ */
+
+public class MrzTest {
+
+ private String valid_id_mrz = "I \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'