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 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 @@ + + + + + + +