From b5daed3dfa5a63aaf236fbf4ee9ce9b3ae783733 Mon Sep 17 00:00:00 2001 From: Arno Onken <asnelt@asnelt.org> Date: Fri, 30 Dec 2016 18:43:38 +0100 Subject: [PATCH] Update to version 2.0. Added support for number type detection. Added support for floating point numbers for Mersenne Twister. Changed app colors. --- Derandom.iml | 17 +- README.md | 22 +- app/app.iml | 92 ++- app/build.gradle | 17 +- app/src/main/AndroidManifest.xml | 10 +- .../derandom/DisplayParametersActivity.java | 3 +- .../org/asnelt/derandom/HistoryBuffer.java | 18 +- .../java/org/asnelt/derandom/HistoryView.java | 61 +- .../derandom/LinearCongruentialGenerator.java | 139 ++-- .../org/asnelt/derandom/MainActivity.java | 77 +- .../org/asnelt/derandom/MersenneTwister.java | 507 +++++++++++- .../org/asnelt/derandom/NumberSequence.java | 722 ++++++++++++++++++ .../asnelt/derandom/NumberSequenceView.java | 78 ++ .../asnelt/derandom/ProcessingFragment.java | 209 ++--- .../org/asnelt/derandom/RandomManager.java | 120 +-- .../derandom/RandomNumberGenerator.java | 119 ++- .../org/asnelt/derandom/SettingsActivity.java | 5 +- .../res/drawable-hdpi/ic_action_discard.png | Bin 450 -> 161 bytes .../res/drawable-hdpi/ic_action_refresh.png | Bin 663 -> 528 bytes .../res/drawable-mdpi/ic_action_discard.png | Bin 324 -> 115 bytes .../res/drawable-mdpi/ic_action_refresh.png | Bin 508 -> 360 bytes .../res/drawable-xhdpi/ic_action_discard.png | Bin 543 -> 151 bytes .../res/drawable-xhdpi/ic_action_refresh.png | Bin 895 -> 665 bytes .../res/drawable-xxhdpi/ic_action_discard.png | Bin 765 -> 194 bytes .../res/drawable-xxhdpi/ic_action_refresh.png | Bin 1239 -> 982 bytes .../drawable-xxxhdpi/ic_action_discard.png | Bin 0 -> 243 bytes .../drawable-xxxhdpi/ic_action_refresh.png | Bin 0 -> 1326 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 39782 bytes .../layout/activity_display_parameters.xml | 2 +- app/src/main/res/layout/activity_main.xml | 32 +- app/src/main/res/layout/activity_settings.xml | 2 +- app/src/main/res/layout/dialog_about.xml | 10 +- app/src/main/res/menu/display_parameters.xml | 2 +- app/src/main/res/menu/main.xml | 7 +- app/src/main/res/menu/settings.xml | 2 +- app/src/main/res/values-v11/styles.xml | 2 +- app/src/main/res/values-v14/styles.xml | 2 +- app/src/main/res/values-v23/styles.xml | 28 + app/src/main/res/values-w820dp/dimens.xml | 2 +- app/src/main/res/values/colors.xml | 29 + app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 11 +- app/src/main/res/values/styles.xml | 28 +- app/src/main/res/xml-v14/preferences.xml | 61 ++ app/src/main/res/xml/backup.xml | 2 +- app/src/main/res/xml/preferences.xml | 2 +- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 17 +- gradlew.bat | 180 ++--- settings.gradle | 2 +- 50 files changed, 2086 insertions(+), 559 deletions(-) create mode 100644 app/src/main/java/org/asnelt/derandom/NumberSequence.java create mode 100644 app/src/main/java/org/asnelt/derandom/NumberSequenceView.java create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_discard.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_refresh.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/values-v23/styles.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/xml-v14/preferences.xml diff --git a/Derandom.iml b/Derandom.iml index 67ecff1..0ebb4bf 100644 --- a/Derandom.iml +++ b/Derandom.iml @@ -1,19 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- -Copyright (C) 2015 Arno Onken - -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. ---> <module external.linked.project.id="Derandom" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <component name="FacetManager"> <facet type="java-gradle" name="Java-Gradle"> @@ -31,4 +16,4 @@ limitations under the License. <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> </component> -</module> +</module> \ No newline at end of file diff --git a/README.md b/README.md index d439d91..5251dae 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ generator like, for instance, the Java standard pseudo random number generator or the Mersenne Twister MT19937. The app will then try to predict following numbers from the generator. -The app expects all numbers to be entered as signed integers. Three -input modes are supported: +The app expects all numbers to be entered as integers or floating point +numbers between zero and one. Currently, floating point numbers are +supported for the Mersenne Twister only. Three input modes are +supported: 1. *Text field* lets you enter the numbers directly on the device. 2. *File* lets you choose a file with newline separated number strings. @@ -64,13 +66,13 @@ s.close() ``` Start the app on the Android device and set the input spinner from *Text field* to *Socket*. Make sure that the device and the Derandom -socket port (default 6869) is reachable in your network. Then set `HOST` -in the Python program to the address of your Android device and run the -program. For each number that is sent by the Python program, eight -predictions are returned by Derandom and displayed by the Python program. -After the app has received 624 numbers the Python Mersenne Twister should -be detected and, in the app, numbers in the prediction history should -appear in green instead of red. +socket port (default 6869) are reachable in your network. Then set +`HOST` in the Python program to the address of your Android device and +run the program. For each number that is sent by the Python program, +eight predictions are returned by Derandom and displayed by the Python +program. After the app has received 624 numbers the Python Mersenne +Twister should be detected and, in the app, numbers in the prediction +history should appear in green instead of red. Building from source @@ -88,7 +90,7 @@ License ------- ```text -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/app.iml b/app/app.iml index 227d02d..37f87bb 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,19 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- -Copyright (C) 2015 Arno Onken - -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. ---> <module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="Derandom" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <component name="FacetManager"> <facet type="android-gradle" name="Android-Gradle"> @@ -27,10 +12,7 @@ limitations under the License. <option name="SELECTED_TEST_ARTIFACT" value="_android_test_" /> <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> - <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" /> - <option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" /> <afterSyncTasks> - <task>generateDebugAndroidTestSources</task> <task>generateDebugSources</task> </afterSyncTasks> <option name="ALLOW_USER_CONFIGURATION" value="false" /> @@ -43,19 +25,21 @@ limitations under the License. </component> <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false"> <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" /> - <output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" /> + <output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/debug" /> <exclude-output /> <content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" /> @@ -65,6 +49,15 @@ limitations under the License. <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/jni" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" /> @@ -72,6 +65,7 @@ limitations under the License. <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> @@ -79,34 +73,60 @@ limitations under the License. <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/jni" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.0.1/jars" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.0.1/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/animated-vector-drawable/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-compat/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-core-ui/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-core-utils/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-fragment/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-media-compat/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/25.1.0/jars" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-vector-drawable/25.1.0/jars" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-resources" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-support" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" /> - <excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/restart-dex" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" /> <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" /> <excludeFolder url="file://$MODULE_DIR$/build/outputs" /> <excludeFolder url="file://$MODULE_DIR$/build/tmp" /> </content> - <orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" /> + <orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" /> <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" exported="" name="appcompat-v7-23.0.1" level="project" /> - <orderEntry type="library" exported="" name="support-v4-23.0.1" level="project" /> - <orderEntry type="library" exported="" name="support-annotations-23.0.1" level="project" /> + <orderEntry type="library" exported="" name="support-compat-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-fragment-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="animated-vector-drawable-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-annotations-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-v4-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-core-ui-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-media-compat-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-vector-drawable-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="appcompat-v7-25.1.0" level="project" /> + <orderEntry type="library" exported="" name="support-core-utils-25.1.0" level="project" /> </component> -</module> +</module> \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a8cba3f..2042f1b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 25 + buildToolsVersion "25.0.2" defaultConfig { applicationId "org.asnelt.derandom" - minSdkVersion 7 - targetSdkVersion 23 + minSdkVersion 9 + targetSdkVersion 25 + return void } buildTypes { @@ -31,9 +32,11 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } } + + return void } dependencies { - compile 'com.android.support:appcompat-v7:23.0.1' - compile 'com.android.support:support-v4:23.0.1' + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:support-v4:25.1.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a1bf0f4..b93bb9a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,9 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="org.asnelt.derandom" - android:versionCode="9" - android:versionName="1.8" > + android:versionCode="10" + android:versionName="2.0" > <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" /> @@ -27,7 +28,8 @@ limitations under the License. android:fullBackupContent="@xml/backup" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:theme="@style/AppTheme" > + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> <activity android:name=".MainActivity" android:label="@string/app_name" diff --git a/app/src/main/java/org/asnelt/derandom/DisplayParametersActivity.java b/app/src/main/java/org/asnelt/derandom/DisplayParametersActivity.java index 3c76964..7b6f3e2 100644 --- a/app/src/main/java/org/asnelt/derandom/DisplayParametersActivity.java +++ b/app/src/main/java/org/asnelt/derandom/DisplayParametersActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,6 +110,7 @@ public class DisplayParametersActivity extends AppCompatActivity { textParameters[i].setInputType(InputType.TYPE_CLASS_NUMBER); // Remove the following line to make fields editable textParameters[i].setKeyListener(null); + textParameters[i].setEnabled(false); layoutParameters.addView(textParameters[i]); } scrollViewParameters.addView(layoutParameters); diff --git a/app/src/main/java/org/asnelt/derandom/HistoryBuffer.java b/app/src/main/java/org/asnelt/derandom/HistoryBuffer.java index 566f019..a0d0db4 100644 --- a/app/src/main/java/org/asnelt/derandom/HistoryBuffer.java +++ b/app/src/main/java/org/asnelt/derandom/HistoryBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.nio.BufferUnderflowException; /** * This class implements a ring buffer for storing long numbers. */ -public class HistoryBuffer { +class HistoryBuffer { /** The maximum number of elements in the buffer. */ private int capacity; /** The array for storing the elements. */ @@ -35,7 +35,7 @@ public class HistoryBuffer { * Constructs an empty buffer with a given capacity. * @param capacity the maximum number of elements the buffer can hold */ - public HistoryBuffer(int capacity) { + HistoryBuffer(int capacity) { clear(); setCapacity(capacity); } @@ -43,7 +43,7 @@ public class HistoryBuffer { /** * Removes all elements from the buffer. */ - public void clear() { + void clear() { head = 0; tail = -1; numbers = new long[0]; @@ -54,7 +54,7 @@ public class HistoryBuffer { * @param capacity the new capacity * @throws IllegalArgumentException if the capacity is less than zero */ - public void setCapacity(int capacity) throws IllegalArgumentException { + void setCapacity(int capacity) throws IllegalArgumentException { if (capacity < 0) { throw new IllegalArgumentException("capacity must not be negative"); } @@ -77,7 +77,7 @@ public class HistoryBuffer { * Puts new elements into the buffer. * @param incomingNumbers the numbers to store */ - public void put(long[] incomingNumbers) { + void put(long[] incomingNumbers) { if (incomingNumbers.length == 0) { return; } @@ -116,7 +116,7 @@ public class HistoryBuffer { * @return the number that was last put into the buffer * @throws BufferUnderflowException if the buffer is empty */ - public long getLast() throws BufferUnderflowException { + long getLast() throws BufferUnderflowException { if (tail < 0) { // Empty buffer throw new BufferUnderflowException(); @@ -130,7 +130,7 @@ public class HistoryBuffer { * @return the numbers that were last put into the buffer * @throws BufferUnderflowException if range is greater than the number of buffer elements */ - public long[] getLast(int range) throws BufferUnderflowException { + private long[] getLast(int range) throws BufferUnderflowException { if (range > length()) { throw new BufferUnderflowException(); } @@ -151,7 +151,7 @@ public class HistoryBuffer { * Returns all elements that are stored in the buffer * @return all elements in the buffer */ - public long[] toArray() { + long[] toArray() { return getLast(length()); } diff --git a/app/src/main/java/org/asnelt/derandom/HistoryView.java b/app/src/main/java/org/asnelt/derandom/HistoryView.java index 76c03ab..2b200b0 100644 --- a/app/src/main/java/org/asnelt/derandom/HistoryView.java +++ b/app/src/main/java/org/asnelt/derandom/HistoryView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,11 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; -import android.widget.TextView; /** * A view for displaying a HistoryBuffer. */ -public class HistoryView extends TextView { +public class HistoryView extends NumberSequenceView { /** * Interface for listening to scroll change events. */ @@ -177,41 +176,36 @@ public class HistoryView extends TextView { setText(getText().toString()); } - /** - * Clears the view. - */ - public void clear() { - setText(""); - } - /** * Appends numbers to the text that is displayed. - * @param numbers numbers to display + * @param numberSequence number sequence to display */ - public void appendNumbers(long[] numbers) { - appendNumbers(numbers, null); + @Override + public void append(NumberSequence numberSequence) { + append(numberSequence, null); } /** - * Appends numbers to the text that is displayed. If correctNumbers is not null and color is - * enabled then the numbers are colored. The numbers are colored green if they match the - * corresponding correctNumbers or red otherwise. - * @param numbers numbers to display - * @param correctNumbers numbers to compare to + * Appends numbers to the text that is displayed. If correctNumberSequence is not null and color + * is enabled then the numbers are colored. The numbers are colored green if they match the + * corresponding correctNumberSequence or red otherwise. + * @param numberSequence number sequence to display + * @param correctNumberSequence numbers to compare to */ - public void appendNumbers(long[] numbers, long[] correctNumbers) { - if (numbers == null || numbers.length == 0) { + public void append(NumberSequence numberSequence, NumberSequence correctNumberSequence) { + if (numberSequence == null || numberSequence.isEmpty()) { return; } + int length = numberSequence.length(); // Number of lines to remove from beginning of textView - int linesToRemove = getLineCount() + numbers.length - capacity; + int linesToRemove = getLineCount() + length - capacity; removeExcessNumbers(linesToRemove); // Offset to first number to append - int offset = numbers.length - capacity; + int offset = length - capacity; if (offset < 0) { offset = 0; } - showNumbers(numbers, correctNumbers, offset); + showNumbers(numberSequence, correctNumberSequence, offset); Layout layout = getLayout(); if (layout != null) { scrollTo(0, layout.getHeight()); @@ -244,28 +238,31 @@ public class HistoryView extends TextView { /** * Shows numbers in the view. If correctNumbers is not null and color is enabled then the * numbers are colored. - * @param numbers the numbers to show - * @param correctNumbers the numbers to compare to + * @param numberSequence the number sequence to show + * @param correctNumberSequence the numbers to compare to * @param offset index of the first number to show */ - private void showNumbers(long[] numbers, long[] correctNumbers, int offset) { - if (numbers == null || numbers.length == 0) { + private void showNumbers(NumberSequence numberSequence, NumberSequence correctNumberSequence, + int offset) { + if (numberSequence == null || numberSequence.isEmpty()) { return; } + int length = numberSequence.length(); // Check whether the numbers should be colored - boolean useColor = colored && correctNumbers != null - && correctNumbers.length >= numbers.length; + boolean useColor = colored && correctNumberSequence != null + && correctNumberSequence.length() >= length; // Check whether we need a newline at the beginning boolean initialNewline = getText().length() > 0; // Append colored numbers - for (int i = offset; i < numbers.length; i++) { + for (int i = offset; i < length; i++) { if (i > offset || initialNewline) { append("\n"); } - String numberString = Long.toString(numbers[i]); + String numberString = numberSequence.toString(i); if (useColor) { Spannable coloredNumberString = new SpannableString(numberString); - if (numbers[i] == correctNumbers[i]) { + if (numberSequence.getInternalNumber(i) + == correctNumberSequence.getInternalNumber(i)) { ForegroundColorSpan colorGreen = new ForegroundColorSpan(Color.GREEN); coloredNumberString.setSpan(colorGreen, 0, coloredNumberString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/app/src/main/java/org/asnelt/derandom/LinearCongruentialGenerator.java b/app/src/main/java/org/asnelt/derandom/LinearCongruentialGenerator.java index 4c8190b..70a3d3e 100644 --- a/app/src/main/java/org/asnelt/derandom/LinearCongruentialGenerator.java +++ b/app/src/main/java/org/asnelt/derandom/LinearCongruentialGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,7 @@ import java.util.List; /** * This class implements a linear congruential random number generator. */ -public class LinearCongruentialGenerator extends RandomNumberGenerator { - /** Human readable name of the generator. */ - private final String name; +class LinearCongruentialGenerator extends RandomNumberGenerator { /** Multiplier parameter. */ private final long multiplier; /** Human readable name of multiplier parameter. */ @@ -54,8 +52,6 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { }; /** The parameter names as a list. */ private static final List PARAMETER_NAMES_LIST = Arrays.asList(PARAMETER_NAMES); - /** Two complement bit extension for negative integers. */ - private static final long COMPLEMENT_INTEGER_EXTENSION = 0xFFFFFFFF00000000L; /** Internal state. */ private volatile long state; /** Index of most significant modulus bit. */ @@ -75,9 +71,9 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { * @param bitRangeStart start index of output bits * @param bitRangeStop stop index of output bits */ - public LinearCongruentialGenerator(String name, long multiplier, long increment, long modulus, - long seed, int bitRangeStart, int bitRangeStop) { - this.name = name; + LinearCongruentialGenerator(String name, long multiplier, long increment, long modulus, + long seed, int bitRangeStart, int bitRangeStop) { + super(name); this.multiplier = multiplier; this.increment = increment; if (modulus == 0L) { @@ -86,7 +82,7 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { this.modulus = modulus; modulusBitRangeStop = Long.SIZE - Long.numberOfLeadingZeros(modulus) - 1; this.initialSeed = seed; - this.state = seed; + setState(seed); // Check index range if (bitRangeStart < 0) { throw new IllegalArgumentException("bitRangeStart must not be negative"); @@ -113,7 +109,8 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { * Resets the generator to its initial seed. */ @Override - public void reset() { + public synchronized void reset() { + super.reset(); setState(initialSeed); } @@ -121,19 +118,10 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { * Sets the state of the generator. * @param state the complete state of the generator */ - public synchronized void setState(long state) { + private synchronized void setState(long state) { this.state = state; } - /** - * Returns the name of the generator. - * @return name of the generator - */ - @Override - public String getName() { - return name; - } - /** * Returns human readable names of all parameters. * @return a string array of parameter names @@ -181,34 +169,57 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { } /** - * Find prediction numbers that match the input series and update the state accordingly. + * Find prediction numbers that match the input sequence and update the state accordingly. * @param incomingNumbers new input numbers * @param historyBuffer previous input numbers - * @return predicted numbers that best match input series + * @return predicted numbers that best match input sequence */ @Override - public synchronized long[] findSeries(long[] incomingNumbers, HistoryBuffer historyBuffer) { - long[] predicted = new long[incomingNumbers.length]; - if (incomingNumbers.length == 0) { - // Empty input - return predicted; - } - // Make prediction based on current state - predicted[0] = next(); - if (predicted[0] != incomingNumbers[0]) { - if (historyBuffer == null || historyBuffer.length() == 0) { - // No history present; just guess incoming number as new state - setState(incomingNumbers[0]); - } else { - // We have a pair to work with - setState(findState(historyBuffer.getLast(), incomingNumbers[0])); + public synchronized NumberSequence findSequence(NumberSequence incomingNumbers, + HistoryBuffer historyBuffer) { + NumberSequence predicted; + if (incomingNumbers == null) { + predicted = new NumberSequence(); + } else if (incomingNumbers.isEmpty()) { + predicted = new NumberSequence(incomingNumbers.getNumberType()); + } else if (incomingNumbers.hasTruncatedOutput()) { + // Make prediction based on current state + predicted = peekNextOutputs(incomingNumbers.length(), incomingNumbers.getNumberType()); + // Check whether the current state is compatible with the incoming numbers + if (predicted.equals(incomingNumbers)) { + return nextOutputs(incomingNumbers.length(), incomingNumbers.getNumberType()); } - } - for (int i = 1; i < incomingNumbers.length; i++) { - predicted[i] = next(); - if (predicted[i] != incomingNumbers[i]) { - setState(findState(incomingNumbers[i-1], incomingNumbers[i])); + // No option found, so disable generator; lattice reduction will solve this problem at + // some point + setActive(false); + } else { + int wordSize = getWordSize(); + long[] incomingWords = incomingNumbers.getSequenceWords(wordSize); + NumberSequence.NumberType numberType = incomingNumbers.getNumberType(); + // Make prediction based on current state + predicted = nextOutputs(incomingNumbers.length(), numberType); + long[] predictedWords = predicted.getSequenceWords(wordSize); + if (predictedWords[0] != incomingWords[0]) { + if (historyBuffer == null || historyBuffer.length() == 0) { + // No history present; just guess incoming number as new state + setState(incomingWords[0]); + } else { + // We have a pair to work with + NumberSequence lastNumber = new NumberSequence( + new long[]{historyBuffer.getLast()}, numberType); + long[] historyWords = lastNumber.getSequenceWords(wordSize); + long lastHistoryWord = historyWords[historyWords.length - 1]; + setState(findState(lastHistoryWord, incomingWords[0])); + } + } + for (int i = 1; i < incomingWords.length && isActive(); i++) { + long nextWord = next(); + predicted.setSequenceWord(i, nextWord, wordSize); + if (nextWord != incomingWords[i]) { + setState(findState(incomingWords[i - 1], incomingWords[i])); + } } + predicted.fixNumberFormat(); } return predicted; } @@ -224,12 +235,43 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { } /** - * Calculate the state of the generator based on two consecutive values. + * Returns the word size of the generator. + * @return the word size + */ + protected int getWordSize() { + return bitRangeStop - bitRangeStart + 1; + } + + /** + * Returns the state of the generator. + * @return the current state + */ + @Override + protected long[] getState() { + long[] state = new long[1]; + state[0] = this.state; + return state; + } + + /** + * Sets the state of the generator. + * @param state the new state + * @throws IllegalArgumentException if state is empty + */ + protected synchronized void setState(long[] state) { + if (state == null || state.length < 1) { + throw new IllegalArgumentException(); + } + this.state = state[0]; + } + + /** + * Calculates the state of the generator based on two consecutive values. * @param number one output of the generator * @param successor next output of the generator * @return the state of the generator after the successor value */ - private long findState(long number, long successor) { + private synchronized long findState(long number, long successor) { // Undo output shift number <<= bitRangeStart; // Number of leading bits that are hidden @@ -258,12 +300,7 @@ public class LinearCongruentialGenerator extends RandomNumberGenerator { * @return the output of the generator */ private long calculateOutput(long state) { - long output = (state & mask) >> bitRangeStart; - // For integers add two complement bit extension for negative numbers - if (bitRangeStop - bitRangeStart + 1 == Integer.SIZE && output >> Integer.SIZE-1 > 0) { - output |= COMPLEMENT_INTEGER_EXTENSION; - } - return output; + return (state & mask) >> bitRangeStart; } /** diff --git a/app/src/main/java/org/asnelt/derandom/MainActivity.java b/app/src/main/java/org/asnelt/derandom/MainActivity.java index 6570756..503aa61 100644 --- a/app/src/main/java/org/asnelt/derandom/MainActivity.java +++ b/app/src/main/java/org/asnelt/derandom/MainActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,8 @@ package org.asnelt.derandom; import android.Manifest; import android.annotation.SuppressLint; -import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -31,6 +32,7 @@ import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.InputType; import android.text.Layout; @@ -84,7 +86,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis /** Field for displaying predictions for previous numbers. */ private HistoryView textHistoryPrediction; /** Field for displaying predictions. */ - private TextView textPrediction; + private NumberSequenceView textPrediction; /** Field for entering input. */ private EditText textInput; /** Spinner for selecting the input method. */ @@ -110,7 +112,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis textHistoryInput.setHistoryViewListener(this); textHistoryPrediction = (HistoryView) findViewById(R.id.text_history_prediction); textHistoryPrediction.setHistoryViewListener(this); - textPrediction = (TextView) findViewById(R.id.text_prediction); + textPrediction = (NumberSequenceView) findViewById(R.id.text_prediction); textInput = (EditText) findViewById(R.id.text_input); spinnerInput = (Spinner) findViewById(R.id.spinner_input); spinnerGenerator = (Spinner) findViewById(R.id.spinner_generator); @@ -127,7 +129,6 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowHomeEnabled(true); - actionBar.setIcon(R.drawable.ic_launcher); } FragmentManager fragmentManager = getSupportFragmentManager(); @@ -268,22 +269,23 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_refresh: - processInput(); + if (processingFragment.getInputSelection() == INDEX_FILE_INPUT) { + selectTextFile(); + } else { + processInput(); + } return true; case R.id.action_discard: clearInput(); return true; case R.id.action_parameters: - openParameters(); + openActivityParameters(); return true; case R.id.action_settings: - openSettings(); + openActivitySettings(); return true; case R.id.action_about: - openAbout(); - return true; - case R.id.action_exit: - finish(); + openDialogAbout(); return true; default: return super.onOptionsItemSelected(item); @@ -393,10 +395,10 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis * @param historyNumbers previously entered numbers * @param historyPredictionNumbers predictions for previous numbers */ - public void onHistoryPredictionReplaced(long[] historyNumbers, - long[] historyPredictionNumbers) { + public void onHistoryPredictionReplaced(NumberSequence historyNumbers, + NumberSequence historyPredictionNumbers) { textHistoryPrediction.clear(); - textHistoryPrediction.appendNumbers(historyPredictionNumbers, historyNumbers); + textHistoryPrediction.append(historyPredictionNumbers, historyNumbers); } /** @@ -413,28 +415,23 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis * @param inputNumbers the entered numbers * @param predictionNumbers predictions for entered numbers */ - public void onHistoryChanged(long[] inputNumbers, long[] predictionNumbers) { + public void onHistoryChanged(NumberSequence inputNumbers, NumberSequence predictionNumbers) { // Appends input numbers to history - textHistoryInput.appendNumbers(inputNumbers); - textHistoryPrediction.appendNumbers(predictionNumbers, inputNumbers); + textHistoryInput.append(inputNumbers); + textHistoryPrediction.append(predictionNumbers, inputNumbers); } /** * Called when the predictions for upcoming numbers changed. * @param predictionNumbers predictions of upcoming numbers */ - public void onPredictionChanged(long[] predictionNumbers) { - textPrediction.setText(""); + public void onPredictionChanged(NumberSequence predictionNumbers) { + textPrediction.clear(); if (predictionNumbers == null) { return; } // Append numbers - for (int i = 0; i < predictionNumbers.length; i++) { - if (i > 0) { - textPrediction.append("\n"); - } - textPrediction.append(Long.toString(predictionNumbers[i])); - } + textPrediction.append(predictionNumbers); textPrediction.scrollTo(0, 0); } @@ -445,7 +442,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis public void onFileInputAborted() { enableDirectInput(); String errorMessage = getResources().getString(R.string.file_error_message); - Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_SHORT).show(); } /** @@ -455,7 +452,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis public void onSocketInputAborted() { enableDirectInput(); String errorMessage = getResources().getString(R.string.socket_error_message); - Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_SHORT).show(); } /** @@ -463,7 +460,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis */ public void onInvalidInputNumber() { String errorMessage = getResources().getString(R.string.number_error_message); - Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_SHORT).show(); } /** @@ -472,7 +469,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis public void onClear() { textHistoryInput.clear(); textHistoryPrediction.clear(); - textPrediction.setText(""); + textPrediction.clear(); if (processingFragment.getInputSelection() == INDEX_DIRECT_INPUT) { // Direct input; reset textInput textInput.setText(""); @@ -547,11 +544,11 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis private void clearInput() { processingFragment.clear(); } - + /** * Show generator parameters in a new activity. Called when the user clicks the parameters item. */ - private void openParameters() { + private void openActivityParameters() { String name = processingFragment.getCurrentGeneratorName(); String[] parameterNames = processingFragment.getCurrentParameterNames(); long[] parameters = processingFragment.getCurrentParameters(); @@ -567,7 +564,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis /** * Called when the user clicks the settings item. */ - private void openSettings() { + private void openActivitySettings() { // Start new settings activity Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); @@ -576,7 +573,7 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis /** * Opens an about dialog. Called when the user clicks the about item. */ - private void openAbout() { + private void openDialogAbout() { // Construct an about dialog String versionName; try { @@ -584,17 +581,25 @@ public class MainActivity extends AppCompatActivity implements OnItemSelectedLis } catch (PackageManager.NameNotFoundException e) { versionName = "unknown"; } + @SuppressLint("InflateParams") View inflater = getLayoutInflater().inflate(R.layout.dialog_about, null); - TextView textVersion = (TextView)inflater.findViewById(R.id.text_version); + TextView textVersion = (TextView) inflater.findViewById(R.id.text_version); textVersion.setText(String.format("%s %s", textVersion.getText().toString(), versionName)); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(R.drawable.ic_launcher); - builder.setTitle("About " + getResources().getString(R.string.app_name)); + builder.setTitle(getResources().getString(R.string.title_dialog_about) + " " + + getResources().getString(R.string.app_name)); builder.setView(inflater); builder.create(); + builder.setPositiveButton(R.string.about_positive, new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); builder.show(); } diff --git a/app/src/main/java/org/asnelt/derandom/MersenneTwister.java b/app/src/main/java/org/asnelt/derandom/MersenneTwister.java index 47cf4df..3bcf589 100644 --- a/app/src/main/java/org/asnelt/derandom/MersenneTwister.java +++ b/app/src/main/java/org/asnelt/derandom/MersenneTwister.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,7 @@ import java.util.concurrent.atomic.AtomicLongArray; /** * This class implements a Mersenne Twister random number generator. */ -public class MersenneTwister extends RandomNumberGenerator { - /** Human readable name of the generator. */ - private final String name; +class MersenneTwister extends RandomNumberGenerator { /** Word size of the generator. */ private final int wordSize; /** Human readable name of word size parameter. */ @@ -100,8 +98,8 @@ public class MersenneTwister extends RandomNumberGenerator { private final long lowerMask; /** Upper mask for state twist transformation. */ private final long upperMask; - /** Two complement bit extension for negative integers. */ - private static final long COMPLEMENT_INTEGER_EXTENSION = 0xFFFFFFFF00000000L; + /** Object for state recovery when the output is partly hidden. */ + private volatile StateFinder stateFinder = null; /** * Constructor initializing all parameters. @@ -121,11 +119,11 @@ public class MersenneTwister extends RandomNumberGenerator { * @param initializationMultiplier the multiplier parameter of the state initialization * @param seed initial seed of the generator */ - public MersenneTwister(String name, int wordSize, int stateSize, int shiftSize, int maskBits, - long twistMask, int temperingU, long temperingD, int temperingS, - long temperingB, int temperingT, long temperingC, int temperingL, - long initializationMultiplier, long seed) { - this.name = name; + MersenneTwister(String name, int wordSize, int stateSize, int shiftSize, int maskBits, + long twistMask, int temperingU, long temperingD, int temperingS, + long temperingB, int temperingT, long temperingC, int temperingL, + long initializationMultiplier, long seed) { + super(name); this.wordSize = wordSize; this.shiftSize = shiftSize; this.maskBits = maskBits; @@ -177,16 +175,9 @@ public class MersenneTwister extends RandomNumberGenerator { */ @Override public synchronized void reset() { + super.reset(); initialize(initialSeed); - } - - /** - * Returns the name of the generator. - * @return name of the generator - */ - @Override - public String getName() { - return name; + stateFinder = null; } /** @@ -237,7 +228,8 @@ public class MersenneTwister extends RandomNumberGenerator { parameters[PARAMETER_NAMES_LIST.indexOf(TEMPERING_T_NAME)] = (long) temperingT; parameters[PARAMETER_NAMES_LIST.indexOf(TEMPERING_C_NAME)] = temperingC; parameters[PARAMETER_NAMES_LIST.indexOf(TEMPERING_L_NAME)] = (long) temperingL; - parameters[PARAMETER_NAMES_LIST.indexOf(INITIALIZATION_MULTIPLIER_NAME)] = initializationMultiplier; + parameters[PARAMETER_NAMES_LIST.indexOf(INITIALIZATION_MULTIPLIER_NAME)] = + initializationMultiplier; return parameters; } @@ -288,22 +280,59 @@ public class MersenneTwister extends RandomNumberGenerator { } /** - * Find prediction numbers that match the input series and update the state accordingly. + * Find prediction numbers that match the input sequence and update the state accordingly. * @param incomingNumbers new input numbers * @param historyBuffer previous input numbers - * @return predicted numbers that best match input series + * @return predicted numbers that best match input sequence */ @Override - public synchronized long[] findSeries(long[] incomingNumbers, HistoryBuffer historyBuffer) { + public synchronized NumberSequence findSequence(NumberSequence incomingNumbers, + HistoryBuffer historyBuffer) { // Make prediction based on current state - long[] predicted = peekNext(incomingNumbers.length); - for (long number : incomingNumbers) { - if (index >= state.length()) { - twistState(state.length()); - index = 0; + NumberSequence predicted = peekNextOutputs(incomingNumbers.length(), + incomingNumbers.getNumberType()); + + // Check whether the current state is compatible with the incoming numbers + if (predicted.equals(incomingNumbers)) { + return nextOutputs(incomingNumbers.length(), incomingNumbers.getNumberType()); + } + + int wordSize = getWordSize(); + long[] incomingWords = incomingNumbers.getSequenceWords(wordSize); + if (incomingNumbers.hasTruncatedOutput()) { + try { + if (stateFinder == null) { + stateFinder = new StateFinder(); + } + boolean solved = false; + long[] observedWordBits = incomingNumbers.getObservedWordBits(wordSize); + for (int i = 0; i < incomingWords.length; i++) { + stateFinder.addInput(incomingWords[i], observedWordBits[i]); + solved = stateFinder.isSolved(); + if (solved) { + // Advance state according to remaining numbers + next(incomingWords.length - i - 1); + stateFinder = null; + break; + } + } + if (!solved) { + return nextOutputs(incomingNumbers.length(), incomingNumbers.getNumberType()); + } + } catch (OutOfMemoryError e) { + stateFinder = null; + setActive(false); + } + } else { + // No hidden output, so tempering can just be reversed + for (long word : incomingWords) { + if (index >= state.length()) { + twistState(state.length()); + index = 0; + } + state.set(index, reverseTemper(word & wordMask)); + index++; } - state.set(index, reverseTemper(number & wordMask)); - index++; } return predicted; } @@ -322,7 +351,53 @@ public class MersenneTwister extends RandomNumberGenerator { } /** - * Initializes the state elements from a seed value.. + * Returns the word size of the generator. + * @return the word size + */ + protected int getWordSize() { + return wordSize; + } + + /** + * Returns the state of the generator. + * @return the current state + */ + @Override + protected long[] getState() { + long[] stateCopy; + try { + stateCopy = new long[state.length() + 1]; + for (int i = 0; i < state.length(); i++) { + stateCopy[i] = state.get(i); + } + stateCopy[stateCopy.length-1] = index; + } catch (OutOfMemoryError e) { + setActive(false); + stateCopy = null; + } + return stateCopy; + } + + /** + * Sets the state of the generator. + * @param newState the new state + * @throws IllegalArgumentException if state does not have enough elements + */ + protected synchronized void setState(long[] newState) { + if (newState == null || newState.length < state.length() + 1) { + throw new IllegalArgumentException(); + } + for (int i = 0; i < state.length(); i++) { + state.set(i, newState[i]); + } + index = (int) newState[newState.length-1]; + if (index < 0 || index > state.length()) { + throw new IllegalArgumentException(); + } + } + + /** + * Initializes the state elements from a seed value. * @param seed the seed value for initialization of the state */ private void initialize(long seed) { @@ -356,12 +431,7 @@ public class MersenneTwister extends RandomNumberGenerator { * @return the output of the generator */ private long emitState(int stateIndex) { - long number = temper(state.get(stateIndex)); - // For integers add two complement bit extension for negative numbers - if (wordSize == Integer.SIZE && number >> Integer.SIZE-1 > 0) { - number |= COMPLEMENT_INTEGER_EXTENSION; - } - return number; + return temper(state.get(stateIndex)); } /** @@ -378,7 +448,8 @@ public class MersenneTwister extends RandomNumberGenerator { } /** - * Reverses the tempering transformation. + * Reverses the tempering transformation by reversing each tempering step. Instead, one could + * also use the transpose (which is the inverse) of the binary tempering matrix. * @param number the output of the tempering transformation * @return the original input to the tempering transformation */ @@ -409,4 +480,360 @@ public class MersenneTwister extends RandomNumberGenerator { } return shifter; } -} \ No newline at end of file + + /** + * Implements a state detection algorithm that can deal with truncated output. The algorithm + * follows Argyros and Kiayias, "I Forgot Your Password: Randomness Attacks Against PHP + * Applications" (2012). + */ + private class StateFinder { + /** This is the binary tempering matrix represented as a long vector. */ + private long[] temperingVector; + /** Use non-sparse coefficients of a single equation for coefficient construction. */ + private long[] equationCoefficients; + /** Sparse representation of equation coefficients of all equations. */ + private long[][] coefficients; + /** The right hand sides of all equations, one bit per equation. */ + private long[] rightHandSide; + /** The total number of equations available so far. */ + private int numberOfEquations; + /** Required number of bits to store a coefficient index. */ + private int bitsPerIndex; + /** Store multiple coefficient indices in each long. */ + private int indicesPerLong; + /** A bit mask to obtain the first coefficient index in a long. */ + private long firstIndexMask; + /** Number of observed numbers in the number sequence. */ + private int sequenceCounter; + /** Flag indicating whether the system of equations is solved. */ + private boolean solved = false; + + /** + * Constructs a state finder initializing all fields and building the tempering matrix. + */ + StateFinder() { + equationCoefficients = new long[(state.length() * wordSize) / Long.SIZE]; + int requiredNumberOfEquations = wordSize * state.length(); + coefficients = new long[requiredNumberOfEquations][]; + rightHandSide = new long[state.length()]; + // Construct tempering matrix + long[] transposedTemperingVector = new long[wordSize]; + long shifter = 1L << (wordSize - 1); + for (int i = 0; i < wordSize; i++) { + transposedTemperingVector[i] = temper(shifter); + shifter >>>= 1; + } + // Bitwise transpose to get a representation in terms of the output + temperingVector = new long[wordSize]; + for (int i = 0; i < wordSize; i++) { + shifter = 1L << (wordSize - 1); + for (int j = 0; j < wordSize; j++) { + // Select bit j of word i + long selectedBit = transposedTemperingVector[i] & shifter; + if (selectedBit != 0) { + // Shift bit j to position i + if (i < j) { + selectedBit <<= j - i; + } else if (j < i) { + selectedBit >>>= i - j; + } + temperingVector[j] |= selectedBit; + } + shifter >>>= 1; + } + } + // Find number of bits required to hold an index + int maximumIndex = requiredNumberOfEquations; + bitsPerIndex = 0; + while (maximumIndex > 0) { + maximumIndex >>>= 1; + bitsPerIndex++; + } + indicesPerLong = Long.SIZE / bitsPerIndex; + firstIndexMask = 0; + for (int i = 0; i < bitsPerIndex; i++) { + firstIndexMask |= (1L << i); + } + sequenceCounter = 0; + // First maskBits state bits are not used + for (int i = 0; i < maskBits; i++) { + coefficients[i] = new long[1]; + coefficients[i][0] = i; + } + numberOfEquations = maskBits; + } + + /** + * Extracts the information from a given number and adds it as equations to the state + * finder. + * @param number the number to process + * @param bits marks the visible bits of number by ones + */ + void addInput(long number, long bits) { + long shifter1 = 1L << (wordSize - 1); + for (int i = 0; i < wordSize; i++) { + if ((bits & shifter1) != 0) { + // Reset equationCoefficients + for (int j = 0; j < equationCoefficients.length; j++) { + equationCoefficients[j] = 0L; + } + // Set coefficients according to temperingVector + long shifter2 = (1L << (wordSize - 1)); + for (int bitIndex = 0; bitIndex < wordSize; bitIndex++) { + if ((temperingVector[i] & shifter2) != 0) { + setEquationCoefficients(equationCoefficients, bitIndex, + sequenceCounter); + } + shifter2 >>>= 1; + } + boolean equationValue = ((number & shifter1) != 0); + insertEquation(equationCoefficients, equationValue); + if (numberOfEquations >= coefficients.length) { + recoverState(sequenceCounter); + break; + } + } + shifter1 >>>= 1; + } + sequenceCounter++; + } + + /** + * Determines whether the system of equations is solved. + * @return true if the system of equations is solved + */ + boolean isSolved() { + return solved; + } + + /** + * Sets the binary equation coefficient at the given sequence index and bit index. + * @param equationCoefficients the equation coefficients to change + * @param bitIndex the bit index of the coefficient to set + * @param sequenceCounter the sequence index of the coefficient to set + */ + private void setEquationCoefficients(long[] equationCoefficients, int bitIndex, + int sequenceCounter) { + if (sequenceCounter < state.length()) { + // Leaf: Flip coefficient at equation bit index + // sequenceCounter * wordSize + bitIndex + int longIndex = (sequenceCounter * wordSize) / Long.SIZE; + int longOffset = (sequenceCounter * wordSize) % Long.SIZE; + equationCoefficients[longIndex] + ^= (1L << (Long.SIZE - 1 - (longOffset + bitIndex))); + } else { + // Recursion + switch (bitIndex) { + case 0: + setEquationCoefficients(equationCoefficients, 0, + sequenceCounter - state.length() + shiftSize); + break; + case 1: + setEquationCoefficients(equationCoefficients, 1, + sequenceCounter - state.length() + shiftSize); + setEquationCoefficients(equationCoefficients, 0, + sequenceCounter - state.length()); + break; + default: + setEquationCoefficients(equationCoefficients, bitIndex, + sequenceCounter - state.length() + shiftSize); + setEquationCoefficients(equationCoefficients, bitIndex - 1, + sequenceCounter - state.length() + 1); + } + long twistMaskBit = twistMask & (1L << (wordSize - bitIndex - 1)); + if (twistMaskBit != 0) { + setEquationCoefficients(equationCoefficients, wordSize - 1, + sequenceCounter - state.length() + 1); + } + } + } + + /** + * Converts a direct representation of the binary coefficients of an equation to a sparse + * one. The sparse representation stores the indices of the non-zero coefficients where + * multiple indices are stored per long. + * @param equationCoefficients the direct representation of the equation coefficients + * @return the sparse representation of the equation coefficients + */ + private long[] convertToSparseCoefficients(long[] equationCoefficients) { + // Count number of non-zero bits + int bitSum = 0; + for (long coefficientLong : equationCoefficients) { + bitSum += Long.bitCount(coefficientLong); + } + long[] sparseCoefficients = new long[(bitSum + indicesPerLong - 1) / indicesPerLong]; + int currentIndex = 0; + for (int longIndex = 0; longIndex < equationCoefficients.length; longIndex++) { + if (equationCoefficients[longIndex] != 0) { + long shifter = 1L << (Long.SIZE - 1); + for (int bitIndex = 0; bitIndex < Long.SIZE; bitIndex++) { + if ((equationCoefficients[longIndex] & shifter) != 0) { + long index = (longIndex * Long.SIZE) + bitIndex; + int subIndex = currentIndex % indicesPerLong; + sparseCoefficients[currentIndex / indicesPerLong] + |= (index << (subIndex * bitsPerIndex)); + currentIndex++; + } + shifter >>>= 1; + } + } + } + return sparseCoefficients; + } + + /** + * Inserts a new equation into the system of equations by converting the equation + * coefficients to a sparse representation and performing online Gaussian elimination. + * @param equationCoefficients direct representation of equation coefficients + * @param equationValue right hand side of the equation + */ + private void insertEquation(long[] equationCoefficients, boolean equationValue) { + // Apply online Gaussian elimination + boolean equationInserted = false; + while (hasAnyCoefficients(equationCoefficients) && !equationInserted) { + int firstCoefficientIndex = getFirstCoefficientIndex(equationCoefficients); + if (coefficients[firstCoefficientIndex] == null) { + // Convert equation into a sparse format + coefficients[firstCoefficientIndex] + = convertToSparseCoefficients(equationCoefficients); + // Store right hand side + if (equationValue) { + flipRightHandSide(firstCoefficientIndex); + } + numberOfEquations++; + equationInserted = true; + } else { + // XOR stored equation with new equation + for (int i = 0; i < coefficients[firstCoefficientIndex].length; i++) { + long indexMask = firstIndexMask; + for (int j = 0; j < indicesPerLong; j++) { + int index = (int) ((coefficients[firstCoefficientIndex][i] & indexMask) + >>> (bitsPerIndex * j)); + // Check whether index is actually set + if (index == 0 && (i > 0 || j > 0)) { + break; + } + // Flip bit at index in equation + equationCoefficients[index / Long.SIZE] + ^= (1L << (Long.SIZE - 1 - (index % Long.SIZE))); + indexMask <<= bitsPerIndex; + } + } + // Check right hand side of stored equation + if (isSetRightHandSide(firstCoefficientIndex)) { + // Flip right hand side of new equation + equationValue = !equationValue; + } + } + } + } + + /** + * Determines whether the binary equation coefficients have any non-zero entry. + * @param equationCoefficients direct representation of equation coefficients + * @return true if the equation coefficients contain any non-zero entry + */ + private boolean hasAnyCoefficients(long[] equationCoefficients) { + for (long coefficientLong : equationCoefficients) { + if (coefficientLong != 0) { + return true; + } + } + return false; + } + + /** + * Returns the index of the first non-zero equation coefficient. + * @param equationCoefficients direct representation of equation coefficients + * @return the index of the first non-zero coefficient + */ + private int getFirstCoefficientIndex(long[] equationCoefficients) { + for (int longIndex = 0; longIndex < equationCoefficients.length; longIndex++) { + if (equationCoefficients[longIndex] != 0) { + long shifter = 1L << (Long.SIZE - 1); + for (int bitIndex = 0; bitIndex < Long.SIZE; bitIndex++) { + if ((equationCoefficients[longIndex] & shifter) != 0) { + return (longIndex * Long.SIZE) + bitIndex; + } + shifter >>>= 1; + } + } + } + return -1; + } + + /** + * Recovers the Mersenne Twister state from the system of equations. First recovers the + * initial state of the generator and then advances the state to the given sequence counter. + * @param sequenceCounter the position in the number sequence to advance the state to + */ + private void recoverState(int sequenceCounter) { + // Eliminate all but one coefficient in each equation + for (int i = coefficients.length - 1; i >= 0; i--) { + if (isSetRightHandSide(i)) { + // Flip right hand side of all equations that contain coefficient j + for (int j = 0; j < i; j++) { + if (hasCoefficient(coefficients[j], i)) { + flipRightHandSide(j); + } + } + // We do not bother to actually flip any coefficient + } + } + // Set initial state + for (int i = 0; i < state.length(); i++) { + state.set(i, rightHandSide[i]); + } + index = 0; + // Advance state according to sequence index + next(sequenceCounter + 1); + solved = true; + } + + /** + * Determines whether the sparse coefficient array contains a given index. + * @param sparseCoefficients sparse coefficients of an equation + * @param index the index to check for + * @return true if the sparse coefficient array contains the given index + */ + private boolean hasCoefficient(long[] sparseCoefficients, int index) { + for (int i = 0; i < sparseCoefficients.length; i++) { + long indexMask = firstIndexMask; + for (int j = 0; j < indicesPerLong; j++) { + int nextIndex = (int) ((sparseCoefficients[i] & indexMask) + >>> (bitsPerIndex * j)); + // Check whether nextIndex is actually set + if (nextIndex == 0 && (i > 0 || j > 0)) { + break; + } + if (nextIndex == index) { + return true; + } + indexMask <<= bitsPerIndex; + } + } + return false; + } + + /** + * Determines whether the right hand side of the equation at the given index is set. + * @param index the index of the equation to check + * @return true if the right hand side of the equation is set + */ + private boolean isSetRightHandSide(int index) { + int longIndex = index / wordSize; + int longOffset = index % wordSize; + return ((rightHandSide[longIndex] & (1L << (wordSize - 1 - longOffset))) != 0); + } + + /** + * Flips the right hand side bit of the equation at the given index. + * @param index the index of the equation to change + */ + private void flipRightHandSide(int index) { + int longIndex = index / wordSize; + int longOffset = index % wordSize; + rightHandSide[longIndex] ^= (1L << (wordSize - 1 - longOffset)); + } + } +} diff --git a/app/src/main/java/org/asnelt/derandom/NumberSequence.java b/app/src/main/java/org/asnelt/derandom/NumberSequence.java new file mode 100644 index 0000000..1342977 --- /dev/null +++ b/app/src/main/java/org/asnelt/derandom/NumberSequence.java @@ -0,0 +1,722 @@ +/* + * Copyright (C) 2015, 2016 Arno Onken + * + * 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 org.asnelt.derandom; + +import java.math.BigInteger; + +/** + * This class represents a sequence of typed numbers. + */ +class NumberSequence { + /** Bit mask for an integer. */ + private static final long INTEGER_MASK = (1L << Integer.SIZE) - 1L; + /** Two complement bit extension for negative integers. */ + private static final long COMPLEMENT_INTEGER_EXTENSION = ~INTEGER_MASK; + /** Number of random bits in a float number. */ + private static final int FLOAT_RANDOM_BITS = 24; + /** Number of random bits in the lower word of a double number. */ + private static final int DOUBLE_LOWER_RANDOM_BITS = 26; + /** Number of random bits in the upper word of a double number. */ + private static final int DOUBLE_UPPER_RANDOM_BITS = 27; + /** Constant for converting long to unsigned long string. */ + private static final BigInteger TWO_COMPLEMENT = BigInteger.ONE.shiftLeft(Long.SIZE); + + /** + * Enumeration of all possible number types. + */ + enum NumberType { + RAW, INTEGER, UNSIGNED_INTEGER, LONG, UNSIGNED_LONG, FLOAT, DOUBLE + } + + /** The number type of the sequence. */ + private NumberType numberType; + /** Internal bitwise representation of the numbers. */ + private long[] internalNumbers; + + /** + * Constructs an empty number sequence with number type raw. + */ + NumberSequence() { + this(NumberType.RAW); + } + + /** + * Constructs an empty number sequence with a given number type. + * @param numberType the number type of the number sequence + */ + NumberSequence(NumberType numberType) { + this.internalNumbers = new long[0]; + this.numberType = numberType; + } + + /** + * Constructs a number sequence from bitwise number representations and a number type. + * @param numbers the numbers in bitwise long format + * @param numberType the number type of the sequence + */ + NumberSequence(long[] numbers, NumberType numberType) { + this.internalNumbers = numbers; + this.numberType = numberType; + } + + /** + * Constructs a number sequence from a string representation and a number type. + * @param numberStrings the numbers in a string representation + * @param numberType the number type of the sequence + */ + NumberSequence(String[] numberStrings, NumberType numberType) { + this.numberType = numberType; + internalNumbers = new long[numberStrings.length]; + // Parse numbers + for (int i = 0; i < internalNumbers.length; i++) { + try { + switch (numberType) { + case RAW: + internalNumbers[i] = parseNumberWithType(numberStrings[i]); + break; + case INTEGER: + internalNumbers[i] = Integer.parseInt(numberStrings[i]); + break; + case LONG: + internalNumbers[i] = Long.parseLong(numberStrings[i]); + break; + case FLOAT: + if (isFloatString(numberStrings[i])) { + internalNumbers[i] = Float.floatToIntBits(Float.parseFloat( + numberStrings[i])); + } else { + internalNumbers[i] = parseNumberWithType(numberStrings[i]); + } + break; + case DOUBLE: + internalNumbers[i] = Double.doubleToLongBits(Double.parseDouble( + numberStrings[i])); + break; + default: + internalNumbers[i] = Long.parseLong(numberStrings[i]); + } + } catch (NumberFormatException e) { + internalNumbers[i] = parseNumberWithType(numberStrings[i]); + } + } + } + + /** + * Formats the numbers in the number sequence to a given number type and sets the number type of + * the sequence. + * @param numberType the number type + */ + void formatNumbers(NumberType numberType) { + formatNumbers(numberType, Long.SIZE); + } + + /** + * Formats the numbers in the number sequence to a given number type and sets the number type of + * the sequence. The internal bitwise representation is interpreted according to the given word + * size. + * @param numberType the number type + * @param wordSize the word size of the internal numbers + */ + void formatNumbers(NumberType numberType, int wordSize) { + // Eventually reverse current format + if (this.numberType != numberType) { + internalNumbers = getSequenceWords(wordSize); + // Apply new format + this.numberType = numberType; + switch (numberType) { + case INTEGER: + case UNSIGNED_INTEGER: + this.internalNumbers = formatIntegers(internalNumbers); + break; + case LONG: + case UNSIGNED_LONG: + this.internalNumbers = assembleLongs(internalNumbers, wordSize); + break; + case FLOAT: + this.internalNumbers = formatFloats(internalNumbers, wordSize); + break; + case DOUBLE: + this.internalNumbers = assembleDoubles(internalNumbers, wordSize); + } + } + } + + /** + * Checks the number format and eventually fixes the complement integer extension. + */ + void fixNumberFormat() { + // Eventually add complement integer extension for negative numbers + if (numberType == NumberType.INTEGER || numberType == NumberType.UNSIGNED_INTEGER) { + internalNumbers = formatIntegers(internalNumbers); + } + } + + /** + * Returns the number type of the number sequence. + * @return the number type of the sequence + */ + NumberType getNumberType() { + return numberType; + } + + /** + * Determines whether the number sequence is empty. + * @return true if number sequence is empty + */ + boolean isEmpty() { + return (internalNumbers == null || internalNumbers.length == 0); + } + + /** + * Determines whether the number sequence is equal to the given number sequence. + * @param numberSequence the number sequence to compare to + * @return true if the number sequences are equal + */ + boolean equals(NumberSequence numberSequence) { + if (length() != numberSequence.length()) { + return false; + } + for (int i = 0; i < length(); i++) { + if (internalNumbers[i] != numberSequence.getInternalNumber(i)) { + return false; + } + } + return true; + } + + /** + * Concatenates the given number sequence to the current sequence. + * @param numberSequence the number sequence to concatenate to the current sequence + * @return the total concatenated number sequence + * @throws NumberFormatException if number types do not match + */ + NumberSequence concatenate(NumberSequence numberSequence) throws NumberFormatException { + if (getNumberType() != numberSequence.getNumberType()) { + throw new NumberFormatException(); + } + int firstLength = length(); + int secondLength = numberSequence.length(); + long[] newInternalNumbers = new long[firstLength + secondLength]; + System.arraycopy(internalNumbers, 0, newInternalNumbers, 0, firstLength); + System.arraycopy(numberSequence.getInternalNumbers(), 0, newInternalNumbers, firstLength, + secondLength); + internalNumbers = newInternalNumbers; + return this; + } + + /** + * Counts the number of matching numbers in the current and the given sequence. + * @param numberSequence the number sequence to compare to + * @return the number of matching numbers + */ + int countMatchesWith(NumberSequence numberSequence) { + int minimumLength = Math.min(length(), numberSequence.length()); + int matches = 0; + for (int i = 0; i < minimumLength; i++) { + if (internalNumbers[i] == numberSequence.getInternalNumber(i)) { + matches++; + } + } + return matches; + } + + /** + * Returns the number of numbers in the sequence. + * @return the number of numbers in the sequence + */ + public int length() { + if (internalNumbers == null) { + return 0; + } else { + return internalNumbers.length; + } + } + + /** + * Returns the bitwise long representation of the number at the given index. + * @param index the index of the number + * @return the bitwise representation of the number + * @throws IndexOutOfBoundsException if index is not a valid index of the sequence + */ + long getInternalNumber(int index) throws IndexOutOfBoundsException { + if (index < 0 || index > internalNumbers.length) { + throw new IndexOutOfBoundsException(); + } + return internalNumbers[index]; + } + + /** + * Returns the whole sequence in its bitwise long representation. + * @return the number sequence in its bitwise long representation + */ + long[] getInternalNumbers() { + return internalNumbers; + } + + /** + * Returns a string representation of the number at the given index. + * @param index the index of the number + * @return a string representation of the number + * @throws IndexOutOfBoundsException if index is not a valid index of the sequence + */ + String toString(int index) throws IndexOutOfBoundsException { + if (internalNumbers == null || index < 0 || index > internalNumbers.length) { + throw new IndexOutOfBoundsException(); + } + String numberString; + switch (numberType) { + case UNSIGNED_LONG: + if (internalNumbers[index] >= 0) { + numberString = Long.toString(internalNumbers[index]); + } else { + // Use BigInteger to convert to unsigned long string + BigInteger bigNumber = BigInteger.valueOf(internalNumbers[index]); + if (bigNumber.signum() < 0) { + bigNumber = bigNumber.add(TWO_COMPLEMENT); + } + numberString = bigNumber.toString(); + } + break; + case FLOAT: + numberString = Float.toString(Float.intBitsToFloat((int) internalNumbers[index])); + break; + case DOUBLE: + numberString = Double.toString(Double.longBitsToDouble(internalNumbers[index])); + break; + default: + numberString = Long.toString(internalNumbers[index]); + } + return numberString; + } + + /** + * Determines whether the number type of the sequence has hidden bits. + * @return true if the number type has truncated bits + */ + boolean hasTruncatedOutput() { + return (numberType == NumberSequence.NumberType.FLOAT + || numberType == NumberSequence.NumberType.DOUBLE); + } + + /** + * Returns the number sequence as a bitwise sequence of words of a given word size. + * @param wordSize the number of bits to represent in a long + * @return the number sequence as a sequence of words + */ + long[] getSequenceWords(int wordSize) { + switch (numberType) { + case INTEGER: + case UNSIGNED_INTEGER: + return reverseFormatIntegers(internalNumbers); + case LONG: + case UNSIGNED_LONG: + return disassembleLongs(internalNumbers, wordSize); + case FLOAT: + return reverseFormatFloats(internalNumbers, wordSize); + case DOUBLE: + return disassembleDoubles(internalNumbers, wordSize); + default: + return internalNumbers; + } + } + + /** + * Sets the word of the number sequence at a given index. + * @param index the index of the word + * @param word the word to write + * @param wordSize the number of bits to represent in a long + */ + void setSequenceWord(int index, long word, int wordSize) { + switch (numberType) { + case LONG: + case UNSIGNED_LONG: + internalNumbers = setLongWord(index, word, internalNumbers, wordSize); + break; + default: + internalNumbers[index] = word; + } + } + + /** + * Returns a sequence of bits in a long array that marks the observed number bits with ones. + * @param wordSize the number of bits to represent in a long + * @return the observed bits of each word of the number sequence marked as ones in a long array + */ + long[] getObservedWordBits(int wordSize) { + long[] observedBits; + long wordMask; + if (wordSize == Long.SIZE) { + wordMask = (Long.MAX_VALUE << 1) | 1L; + } else { + wordMask = (1L << wordSize) - 1L; + } + switch (numberType) { + case INTEGER: + case UNSIGNED_INTEGER: + observedBits = new long[internalNumbers.length]; + for (int i = 0; i < observedBits.length; i++) { + observedBits[i] = wordMask & INTEGER_MASK; + } + break; + case LONG: + case UNSIGNED_LONG: + if (wordSize > Integer.SIZE) { + observedBits = new long[internalNumbers.length]; + for (int i = 0; i < observedBits.length; i++) { + observedBits[i] = wordMask; + } + } else { + observedBits = new long[internalNumbers.length * 2]; + for (int i = 0; i < observedBits.length; i++) { + observedBits[i] = wordMask; + } + } + break; + case FLOAT: + observedBits = new long[internalNumbers.length]; + long floatMask = ((1L << FLOAT_RANDOM_BITS) - 1L) << (wordSize - FLOAT_RANDOM_BITS); + for (int i = 0; i < internalNumbers.length; i++) { + observedBits[i] = floatMask; + } + break; + case DOUBLE: + observedBits = new long[internalNumbers.length * 2]; + long doubleLowerMask = ((1L << DOUBLE_LOWER_RANDOM_BITS) - 1L) + << (wordSize - DOUBLE_LOWER_RANDOM_BITS); + long doubleUpperMask = ((1L << DOUBLE_UPPER_RANDOM_BITS) - 1L) + << (wordSize - DOUBLE_UPPER_RANDOM_BITS); + for (int i = 0; i < internalNumbers.length; i++) { + observedBits[2 * i] = doubleUpperMask; + observedBits[2 * i + 1] = doubleLowerMask; + } + break; + default: + observedBits = new long[internalNumbers.length]; + for (int i = 0; i < observedBits.length; i++) { + observedBits[i] = wordMask; + } + } + return observedBits; + } + + /** + * Returns the number of words required for each number of the sequence. + * @return the number of words required for a number of the sequence + */ + static int getRequiredWordsPerNumber(NumberType numberType) { + switch (numberType) { + case LONG: + case UNSIGNED_LONG: + case DOUBLE: + return 2; + default: + return 1; + } + } + + /** + * Parses the number string and returns a bitwise long representation of that number. Also tries + * to detect the number type automatically and sets the internal type of the sequence + * accordingly. + * @param numberString the number represented as a string + * @return the bitwise long representation of the number + * @throws NumberFormatException if the number type of the number string is incompatible + */ + private long parseNumberWithType(String numberString) throws NumberFormatException { + // Try all possible number types and eventually change input type + // Throw NumberFormatException if no number type fits + long number; + NumberSequence.NumberType currentType = getNumberType(); + if (currentType == NumberSequence.NumberType.FLOAT + || getNumberType() == NumberSequence.NumberType.DOUBLE + || numberString.contains(".")) { + if (isFloatString(numberString)) { + float value = Float.parseFloat(numberString); + if (currentType != NumberSequence.NumberType.FLOAT) { + numberType = NumberType.FLOAT; + } + number = Float.floatToIntBits(value); + } else { + double value = Double.parseDouble(numberString); + if (currentType != NumberSequence.NumberType.DOUBLE) { + numberType = NumberType.DOUBLE; + } + number = Double.doubleToLongBits(value); + } + } else { + BigInteger bigNumber = new BigInteger(numberString); + number = bigNumber.longValue(); + // Check whether type is already at unsigned long but next number is negative + if (currentType == NumberSequence.NumberType.UNSIGNED_LONG + && bigNumber.signum() < 0) { + throw new NumberFormatException(); + } + // Find minimum range that can hold the number + if (number >= Integer.MIN_VALUE && number <= Integer.MAX_VALUE) { + if (currentType == NumberSequence.NumberType.RAW) { + numberType = NumberType.INTEGER; + } + } else if (bigNumber.signum() >= 0 && number < (1L << Integer.SIZE)) { + if (currentType == NumberSequence.NumberType.RAW + || currentType == NumberSequence.NumberType.INTEGER) { + numberType = NumberType.UNSIGNED_INTEGER; + } + } else { + if (number >= 0 || bigNumber.signum() < 0) { + if (currentType == NumberSequence.NumberType.RAW + || currentType == NumberSequence.NumberType.INTEGER + || currentType == NumberSequence.NumberType.UNSIGNED_INTEGER) + { + numberType = NumberType.LONG; + } + } else { + // The most significant bit is set, but the number is not negative + numberType = NumberType.UNSIGNED_LONG; + } + } + } + return number; + } + + /** + * Determines whether float precision is sufficient to represent the number represented by the + * input string. + * @param inputString the string representation of the number to check + * @return true if float precision is sufficient + */ + private boolean isFloatString(String inputString) { + // Test whether value fits into a float + double doubleValue, floatValue; + try { + doubleValue = Double.parseDouble(inputString); + String floatString = Float.toString(Float.parseFloat(inputString)); + floatValue = Double.parseDouble(floatString); + } catch (NumberFormatException e) { + return false; + } + return (doubleValue == floatValue); + } + + /** + * For each long value in the input array, takes the bits that fit into an int and eventually + * adds bits for the two complement bit extension. + * @param values the long values to format + * @return the integer values in a long array + */ + private long[] formatIntegers(long[] values) { + long[] numbers = new long[values.length]; + for (int i = 0; i < values.length; i++) { + numbers[i] = values[i] & INTEGER_MASK; + } + if (numberType == NumberSequence.NumberType.INTEGER) { + // Add two complement bit extension for negative numbers + for (int i = 0; i < numbers.length; i++) { + if ((numbers[i] >> Integer.SIZE - 1) > 0) { + numbers[i] |= COMPLEMENT_INTEGER_EXTENSION; + } + } + } + return numbers; + } + + /** + * Eventually removes the two complement bit extension for negative numbers. + * @param values the long array containing the integer values + * @return the long array without the bits from the two complement extension + */ + private long[] reverseFormatIntegers(long[] values) { + long[] words = new long[values.length]; + for (int i = 0; i < values.length; i++) { + words[i] = values[i] & INTEGER_MASK; + } + return words; + } + + /** + * Assembles a sequence of long numbers from an array of words. + * @param words the words to assemble longs from + * @param wordSize the number of bits to represent in a long + * @return the array of long numbers + */ + private long[] assembleLongs(long[] words, int wordSize) { + long[] numbers = new long[wordSize > Integer.SIZE ? words.length : (words.length / 2)]; + for (int i = 0; i < words.length; i++) { + setLongWord(i, words[i], numbers, wordSize); + } + return numbers; + } + + /** + * This is the inverse function of assembleLongs. + * @param numbers an array of long numbers + * @param wordSize the number of bits to represent in a long + * @return the words of the long array where each word is stored in one long + */ + private long[] disassembleLongs(long[] numbers, int wordSize) { + long[] words = new long[wordSize > Integer.SIZE ? + numbers.length : (numbers.length * 2)]; + for (int i = 0; i < words.length; i++) { + words[i] = getLongWord(i, numbers, wordSize); + } + return words; + } + + /** + * Returns the word at the given index from an array of long numbers. + * @param index the index of the word + * @param numbers an array of long numbers + * @param wordSize the number of bits to represent in a long + * @return the word stored in a long + */ + private long getLongWord(int index, long[] numbers, int wordSize) { + if (wordSize > Integer.SIZE) { + return numbers[index]; + } else { + int numberIndex = index / 2; + if (index % 2 == 0) { + // Even index, so return upper word + return numbers[numberIndex] >> Integer.SIZE; + } else { + // Uneven index, so return lower word + return numbers[numberIndex] & INTEGER_MASK; + } + } + } + + /** + * Sets the word at the given index in an array of long numbers. + * @param index the index of the word to set + * @param word the word to be written + * @param numbers an array of long numbers + * @param wordSize the number of bits to represent in a long + * @return the array of long numbers with the overwritten word + */ + private long[] setLongWord(int index, long word, long[] numbers, int wordSize) { + if (wordSize > Integer.SIZE) { + numbers[index] = word; + } else { + int numberIndex = index / 2; + if (index % 2 == 0) { + // Even index, so set upper word + numbers[numberIndex] = (word << Integer.SIZE) + + (numbers[numberIndex] & INTEGER_MASK); + } else { + // Uneven index, so set lower word + numbers[numberIndex] = word + (numbers[numberIndex] & COMPLEMENT_INTEGER_EXTENSION); + } + } + return numbers; + } + + /** + * Format a sequence of float numbers from an array of words. + * @param words the words to assemble floats from + * @param wordSize the number of bits to represent in a long + * @return the sequence of floats bitwise represented as an array of longs + */ + private long[] formatFloats(long[] words, int wordSize) { + long[] values = new long[words.length]; + int shiftSize = wordSize - FLOAT_RANDOM_BITS; + for (int i = 0; i < words.length; i++) { + float nextValue = (words[i] >>> shiftSize) / ((float) (1 << FLOAT_RANDOM_BITS)); + values[i] = Float.floatToIntBits(nextValue); + } + return values; + } + + /** + * This is the inverse function of formatFloats. + * @param values sequence of floats bitwise represented as an array of longs + * @param wordSize the number of bits to represent in a long + * @return the words of the sequence of float numbers + */ + private long[] reverseFormatFloats(long[] values, int wordSize) { + long[] words = new long[values.length]; + int shiftSize = wordSize - FLOAT_RANDOM_BITS; + for (int i = 0; i < values.length; i++) { + float nextValue = Float.intBitsToFloat((int) values[i]); + nextValue *= (float) (1 << FLOAT_RANDOM_BITS); + words[i] = (int) nextValue; + words[i] <<= shiftSize; + } + return words; + } + + /** + * Assembles a sequence of double numbers from an array of words. + * @param words the words to assemble doubles from + * @param wordSize the number of bits to represent in a long + * @return the sequence of doubles bitwise represented as an array of longs + */ + private long[] assembleDoubles(long[] words, int wordSize) { + long[] values; + if (wordSize > Float.SIZE) { + values = new long[words.length]; + int shiftSize = wordSize - (DOUBLE_LOWER_RANDOM_BITS + DOUBLE_UPPER_RANDOM_BITS); + for (int i = 0; i < words.length; i++) { + double nextValue = (words[i] >>> shiftSize) + / ((double) (1L << (DOUBLE_LOWER_RANDOM_BITS + DOUBLE_UPPER_RANDOM_BITS))); + values[i] = Double.doubleToLongBits(nextValue); + } + } else { + int lowerShiftSize = wordSize - DOUBLE_LOWER_RANDOM_BITS; + int upperShiftSize = wordSize - DOUBLE_UPPER_RANDOM_BITS; + values = new long[words.length / 2]; + for (int i = 0; i < values.length; i++) { + double nextValue = (((words[i * 2] >>> upperShiftSize) << DOUBLE_LOWER_RANDOM_BITS) + + (words[i * 2 + 1] >>> lowerShiftSize)) + / ((double) (1L << (DOUBLE_LOWER_RANDOM_BITS + DOUBLE_UPPER_RANDOM_BITS))); + values[i] = Double.doubleToLongBits(nextValue); + } + } + return values; + } + + /** + * This is the inverse function of assembleDoubles. + * @param values a sequence of doubles bitwise represented as an array of longs + * @param wordSize the number of bits to represent in a long + * @return the words of the sequence of double numbers + */ + private long[] disassembleDoubles(long[] values, int wordSize) { + long[] words; + if (wordSize > Float.SIZE) { + words = new long[values.length]; + int shiftSize = wordSize - (DOUBLE_LOWER_RANDOM_BITS + DOUBLE_UPPER_RANDOM_BITS); + for (int i = 0; i < values.length; i++) { + double nextValue = Double.longBitsToDouble(values[i]); + nextValue *= (double) (1L << (DOUBLE_LOWER_RANDOM_BITS + DOUBLE_UPPER_RANDOM_BITS)); + words[i] = (long) nextValue; + words[i] <<= shiftSize; + } + } else { + int lowerShiftSize = wordSize - DOUBLE_LOWER_RANDOM_BITS; + int upperShiftSize = wordSize - DOUBLE_UPPER_RANDOM_BITS; + long doubleLowerMask = (1L << DOUBLE_LOWER_RANDOM_BITS) - 1L; + words = new long[values.length * 2]; + for (int i = 0; i < values.length; i++) { + double nextValue = Double.longBitsToDouble(values[i]); + nextValue *= (double) (1L << (DOUBLE_LOWER_RANDOM_BITS + DOUBLE_UPPER_RANDOM_BITS)); + words[2 * i] = (long) nextValue; + words[2 * i] = (words[2 * i] >>> DOUBLE_LOWER_RANDOM_BITS) << upperShiftSize; + words[2 * i + 1] = (long) nextValue; + words[2 * i + 1] = (words[2 * i + 1] & doubleLowerMask) << lowerShiftSize; + } + } + return words; + } +} diff --git a/app/src/main/java/org/asnelt/derandom/NumberSequenceView.java b/app/src/main/java/org/asnelt/derandom/NumberSequenceView.java new file mode 100644 index 0000000..1bb17b3 --- /dev/null +++ b/app/src/main/java/org/asnelt/derandom/NumberSequenceView.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015, 2016 Arno Onken + * + * 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 org.asnelt.derandom; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * A vew for displaying a number sequence. + */ +public class NumberSequenceView extends TextView { + /** + * Standard constructor for a NumberSequenceView. + * @param context global information about an application environment + */ + public NumberSequenceView(Context context) { + super(context); + } + + /** + * Standard constructor for a NumberSequenceView. + * @param context global information about an application environment + * @param attributeSet collection of attributes + */ + public NumberSequenceView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + /** + * Standard constructor for a NumberSequenceView. + * @param context global information about an application environment + * @param attributeSet collection of attributes + * @param defaultStyledAttributes default values for styled attributes + */ + public NumberSequenceView(Context context, AttributeSet attributeSet, + int defaultStyledAttributes) { + super(context, attributeSet, defaultStyledAttributes); + } + + /** + * Clears the view. + */ + public void clear() { + setText(""); + } + + /** + * Appends a number sequence to the view. + * @param numberSequence the number sequence to append to the view + */ + public void append(NumberSequence numberSequence) { + if (numberSequence == null) { + return; + } + // Append numbers + for (int i = 0; i < numberSequence.length(); i++) { + if (i > 0) { + append("\n"); + } + append(numberSequence.toString(i)); + } + } +} diff --git a/app/src/main/java/org/asnelt/derandom/ProcessingFragment.java b/app/src/main/java/org/asnelt/derandom/ProcessingFragment.java index 45a0716..be9176d 100644 --- a/app/src/main/java/org/asnelt/derandom/ProcessingFragment.java +++ b/app/src/main/java/org/asnelt/derandom/ProcessingFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,8 @@ public class ProcessingFragment extends Fragment { * @param historyNumbers previously entered numbers * @param historyPredictionNumbers predictions for previous numbers */ - void onHistoryPredictionReplaced(long[] historyNumbers, long[] historyPredictionNumbers); + void onHistoryPredictionReplaced(NumberSequence historyNumbers, + NumberSequence historyPredictionNumbers); /** * Called when the random number generator selection changed. @@ -67,13 +68,13 @@ public class ProcessingFragment extends Fragment { * @param inputNumbers the entered numbers * @param predictionNumbers predictions for entered numbers */ - void onHistoryChanged(long[] inputNumbers, long[] predictionNumbers); + void onHistoryChanged(NumberSequence inputNumbers, NumberSequence predictionNumbers); /** * Called when the predictions for upcoming numbers changed. * @param predictionNumbers predictions of upcoming numbers */ - void onPredictionChanged(long[] predictionNumbers); + void onPredictionChanged(NumberSequence predictionNumbers); /** * Called when setting the input method to an input file is aborted and sets the input @@ -140,13 +141,15 @@ public class ProcessingFragment extends Fragment { /** Number of process input tasks. */ private volatile int inputTaskLength; /** Flag for whether processing should continue. */ - private volatile boolean processingDesirable; + private volatile boolean processingEnabled; /** Server socket port. */ private volatile int serverPort; /** Server socket. */ - private ServerSocket serverSocket; + private volatile ServerSocket serverSocket; /** Client socket. */ private volatile Socket clientSocket; + /** Current number type. */ + private volatile NumberSequence.NumberType numberType; /** Listener for processing changes. */ private ProcessingFragmentListener listener; /** Future for cancelling the server task. */ @@ -167,8 +170,9 @@ public class ProcessingFragment extends Fragment { outputWriter = null; missingUpdate = false; inputSelection = 0; + numberType = NumberSequence.NumberType.RAW; inputTaskLength = 0; - processingDesirable = true; + processingEnabled = true; serverPort = 0; clientSocket = null; synchronizationObject = this; @@ -218,12 +222,12 @@ public class ProcessingFragment extends Fragment { /** * Sets the currently active generator. - * @param number index of the currently active generator + * @param index index of the currently active generator */ - public void setCurrentGenerator(int number) { - if (number != randomManager.getCurrentGenerator()) { + public void setCurrentGenerator(int index) { + if (index != randomManager.getCurrentGeneratorIndex()) { prepareInputProcessing(); - processingExecutor.execute(new UpdateAllTask(number)); + processingExecutor.execute(new UpdateAllTask(index)); } } @@ -343,7 +347,9 @@ public class ProcessingFragment extends Fragment { * Executes a clear task. */ public void clear() { - processingDesirable = false; + randomManager.deactivateAll(); + processingEnabled = false; + numberType = NumberSequence.NumberType.RAW; processingExecutor.execute(new ClearTask()); } @@ -462,7 +468,7 @@ public class ProcessingFragment extends Fragment { */ @Override public void onDestroy() { - processingDesirable = false; + processingEnabled = false; // Shutdown server thread serverExecutor.shutdownNow(); // Shutdown processing thread @@ -513,9 +519,9 @@ public class ProcessingFragment extends Fragment { @Override public void run() { historyBuffer.clear(); - int currentGenerator = randomManager.getCurrentGenerator(); + int currentGeneratorIndex = randomManager.getCurrentGeneratorIndex(); randomManager.reset(); - randomManager.setCurrentGenerator(currentGenerator); + randomManager.setCurrentGeneratorIndex(currentGeneratorIndex); boolean posted = handler.post(new Runnable() { @Override public void run() { @@ -530,7 +536,7 @@ public class ProcessingFragment extends Fragment { if (!posted) { missingUpdate = true; } - processingDesirable = true; + processingEnabled = true; } } @@ -544,7 +550,7 @@ public class ProcessingFragment extends Fragment { /** * Constructor for setting the new input capacity. */ - public ChangeCapacityTask(final int capacity) { + ChangeCapacityTask(final int capacity) { this.capacity = capacity; } @@ -567,18 +573,18 @@ public class ProcessingFragment extends Fragment { private final boolean changeGenerator; /** - * Standard constructor that initializes a task that does not change the generator.. + * Standard constructor that initializes a task that does not change the generator. */ - public UpdateAllTask() { + UpdateAllTask() { this.generatorIndex = 0; this.changeGenerator = false; } /** - * Constructor that initializes a task that does change the generator. + * Constructor that initializes a task that changes the generator. * @param generatorIndex index of the new generator */ - public UpdateAllTask(final int generatorIndex) { + UpdateAllTask(final int generatorIndex) { this.generatorIndex = generatorIndex; this.changeGenerator = true; } @@ -589,23 +595,23 @@ public class ProcessingFragment extends Fragment { @Override public void run() { final boolean generatorChanged; - if (changeGenerator && randomManager.getCurrentGenerator() != generatorIndex) { + if (changeGenerator && randomManager.getCurrentGeneratorIndex() != generatorIndex) { // Process complete history - randomManager.setCurrentGenerator(generatorIndex); + randomManager.setCurrentGeneratorIndex(generatorIndex); generatorChanged = true; } else { generatorChanged = false; } - final long[] historyNumbers; - final long[] historyPredictionNumbers; - final long[] predictionNumbers; + final NumberSequence historyNumbers; + final NumberSequence historyPredictionNumbers; + final NumberSequence predictionNumbers; if ((generatorChanged || !changeGenerator) && historyBuffer.length() > 0) { randomManager.resetCurrentGenerator(); - randomManager.findCurrentSeries(historyBuffer.toArray(), null); - historyNumbers = historyBuffer.toArray(); + historyNumbers = new NumberSequence(historyBuffer.toArray(), numberType); + randomManager.findCurrentSequence(historyNumbers, null); historyPredictionNumbers = randomManager.getIncomingPredictionNumbers(); // Generate new prediction without updating the state - predictionNumbers = randomManager.predict(predictionLength); + predictionNumbers = randomManager.predict(predictionLength, numberType); } else { historyNumbers = null; historyPredictionNumbers = null; @@ -647,7 +653,7 @@ public class ProcessingFragment extends Fragment { * Constructor for processing an input string. * @param input the input string to be processed */ - public ProcessInputTask(final String input) { + ProcessInputTask(final String input) { this.input = input; this.fileUri = null; } @@ -656,7 +662,7 @@ public class ProcessingFragment extends Fragment { * Constructor for processing the input file pointed to by fileUri. * @param fileUri the URI of the file to be processed */ - public ProcessInputTask(final Uri fileUri) { + ProcessInputTask(final Uri fileUri) { this.input = null; this.fileUri = fileUri; } @@ -664,7 +670,7 @@ public class ProcessingFragment extends Fragment { /** * Constructor for processing input from current input reader. */ - public ProcessInputTask() { + ProcessInputTask() { this.input = null; this.fileUri = null; } @@ -720,7 +726,7 @@ public class ProcessingFragment extends Fragment { return; } try { - while (inputReader.ready() && processingDesirable) { + while (inputReader.ready() && processingEnabled) { String nextInput = inputReader.readLine(); if (nextInput == null) { break; @@ -799,72 +805,73 @@ public class ProcessingFragment extends Fragment { } /** - * Processes the given input string by parsing the numbers and searching for compatible - * generator states. The generator is eventually changed if the flag autoDetect is set and a - * better generator is detected.. - * @param input string of newline separated integers - * @throws NumberFormatException if input contains an invalid number string + * Processes the given input string by parsing the input string and processing the numbers. + * @param inputString string of newline separated integers */ - private void processInputString(String input) throws NumberFormatException { - long[] inputNumbers; - String[] stringNumbers = input.split("\n"); - inputNumbers = new long[stringNumbers.length]; - // Parse numbers - for (int i = 0; i < inputNumbers.length; i++) { - inputNumbers[i] = Long.parseLong(stringNumbers[i]); + private void processInputString(String inputString) { + NumberSequence inputNumbers; + String[] stringNumbers = inputString.split("\n"); + inputNumbers = new NumberSequence(stringNumbers, numberType); + NumberSequence.NumberType inputNumberType = inputNumbers.getNumberType(); + if (inputNumberType != numberType) { + // Reformat history numbers + NumberSequence historyNumbers = new NumberSequence(historyBuffer.toArray(), + numberType); + historyNumbers.formatNumbers(inputNumberType); + historyBuffer.clear(); + numberType = inputNumberType; + inputNumbers = historyNumbers.concatenate(inputNumbers); + showClear(); } - long[] historyPredictionNumbers; - long[] historyNumbers = null; - long[] replacedNumbers = null; - int bestGenerator = 0; - boolean generatorChanged = false; + processInputNumbers(inputNumbers); + } + + /** + * Processes the given input numbers searching for compatible generator states. The + * generator is eventually changed if the flag autoDetect is set and a better generator is + * detected. + * @param inputNumbers the number sequence to be processed + */ + private void processInputNumbers(NumberSequence inputNumbers) { + NumberSequence historyPredictionNumbers; if (autoDetect) { // Detect best generator and update all states - bestGenerator = randomManager.detectGenerator(inputNumbers, historyBuffer); + int bestGenerator = randomManager.detectGenerator(inputNumbers, historyBuffer); historyPredictionNumbers = randomManager.getIncomingPredictionNumbers(); - if (bestGenerator != randomManager.getCurrentGenerator()) { + if (bestGenerator != randomManager.getCurrentGeneratorIndex()) { // Set generator and process complete history - randomManager.setCurrentGenerator(bestGenerator); + randomManager.setCurrentGeneratorIndex(bestGenerator); randomManager.resetCurrentGenerator(); - historyNumbers = historyBuffer.toArray(); - randomManager.findCurrentSeries(historyNumbers, null); - replacedNumbers = randomManager.getIncomingPredictionNumbers(); - randomManager.findCurrentSeries(inputNumbers, historyBuffer); + NumberSequence historyNumbers = new NumberSequence(historyBuffer.toArray(), + numberType); + randomManager.findCurrentSequence(historyNumbers, null); + NumberSequence replacedNumbers = randomManager.getIncomingPredictionNumbers(); + randomManager.findCurrentSequence(inputNumbers, historyBuffer); historyPredictionNumbers = randomManager.getIncomingPredictionNumbers(); - generatorChanged = true; + // Post change to user interface + showGeneratorChange(historyNumbers, replacedNumbers, bestGenerator); } } else { - randomManager.findCurrentSeries(inputNumbers, historyBuffer); + randomManager.findCurrentSequence(inputNumbers, historyBuffer); historyPredictionNumbers = randomManager.getIncomingPredictionNumbers(); } // Generate new prediction without updating the state - long[] predictionNumbers = randomManager.predict(predictionLength); - historyBuffer.put(inputNumbers); + NumberSequence predictionNumbers = randomManager.predict(predictionLength, numberType); + historyBuffer.put(inputNumbers.getInternalNumbers()); // Post result to user interface - if (generatorChanged) { - showGeneratorChange(inputNumbers, historyPredictionNumbers, predictionNumbers, - historyNumbers, replacedNumbers, bestGenerator); - } else { - showInputUpdate(inputNumbers, historyPredictionNumbers, predictionNumbers); - } + showInputUpdate(inputNumbers, historyPredictionNumbers, predictionNumbers); } /** - * Sends the processing result to the processing listener. - * @param inputNumbers the processed input numbers - * @param historyPredictionNumbers the prediction numbers corresponding to the input - * @param predictionNumbers the predicted numbers + * Sends the instruction to clear to the processing listener. */ - private void showInputUpdate(final long[] inputNumbers, - final long[] historyPredictionNumbers, - final long[] predictionNumbers) { + private void showClear() { boolean posted = handler.post(new Runnable() { @Override public void run() { - // Append input numbers to history + // Clear all fields if (listener != null) { - listener.onHistoryChanged(inputNumbers, historyPredictionNumbers); - listener.onPredictionChanged(predictionNumbers); + listener.onClear(); } else { missingUpdate = true; } @@ -873,24 +880,16 @@ public class ProcessingFragment extends Fragment { if (!posted) { missingUpdate = true; } - writeSocketOutput(predictionNumbers); } /** - * Sends the processing result to the processing listener. The result includes a change of - * generator. - * @param inputNumbers the processed input numbers - * @param historyPredictionNumbers the prediction numbers corresponding to the input - * @param predictionNumbers the predicted numbers + * Sends the generator change to the processing listener. * @param historyNumbers the complete previous input * @param replacedNumbers the complete previous prediction numbers * @param bestGenerator index of the best generator */ - private void showGeneratorChange(final long[] inputNumbers, - final long[] historyPredictionNumbers, - final long[] predictionNumbers, - final long[] historyNumbers, - final long[] replacedNumbers, + private void showGeneratorChange(final NumberSequence historyNumbers, + final NumberSequence replacedNumbers, final int bestGenerator) { boolean posted = handler.post(new Runnable() { @Override @@ -899,6 +898,30 @@ public class ProcessingFragment extends Fragment { if (listener != null) { listener.onGeneratorChanged(bestGenerator); listener.onHistoryPredictionReplaced(historyNumbers, replacedNumbers); + } else { + missingUpdate = true; + } + } + }); + if (!posted) { + missingUpdate = true; + } + } + + /** + * Sends the processing result to the processing listener. + * @param inputNumbers the processed input numbers + * @param historyPredictionNumbers the prediction numbers corresponding to the input + * @param predictionNumbers the predicted numbers + */ + private void showInputUpdate(final NumberSequence inputNumbers, + final NumberSequence historyPredictionNumbers, + final NumberSequence predictionNumbers) { + boolean posted = handler.post(new Runnable() { + @Override + public void run() { + // Append input numbers to history + if (listener != null) { listener.onHistoryChanged(inputNumbers, historyPredictionNumbers); listener.onPredictionChanged(predictionNumbers); } else { @@ -917,12 +940,12 @@ public class ProcessingFragment extends Fragment { * the prediction block. * @param predictionNumbers the predicted numbers */ - private void writeSocketOutput(long[] predictionNumbers) { + private void writeSocketOutput(NumberSequence predictionNumbers) { if (outputWriter != null && predictionNumbers != null) { try { // Write numbers to output stream - for (long number : predictionNumbers) { - outputWriter.write(Long.toString(number)); + for (int i = 0; i < predictionNumbers.length(); i++) { + outputWriter.write(predictionNumbers.toString(i)); outputWriter.newLine(); } // Finish this sequence of numbers with an additional newline @@ -945,9 +968,9 @@ public class ProcessingFragment extends Fragment { @Override public void run() { // Generate new prediction without updating the state - final long[] predictionNumbers; + final NumberSequence predictionNumbers; if (historyBuffer.length() > 0) { - predictionNumbers = randomManager.predict(predictionLength); + predictionNumbers = randomManager.predict(predictionLength, numberType); } else { predictionNumbers = null; } diff --git a/app/src/main/java/org/asnelt/derandom/RandomManager.java b/app/src/main/java/org/asnelt/derandom/RandomManager.java index da957a1..5242eed 100644 --- a/app/src/main/java/org/asnelt/derandom/RandomManager.java +++ b/app/src/main/java/org/asnelt/derandom/RandomManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray; /** * Manages all random number generators. */ -public class RandomManager { +class RandomManager { /** Random number generators. */ private volatile AtomicReferenceArray<RandomNumberGenerator> generators; /** Names of all linear congruential generators. */ @@ -233,26 +233,26 @@ public class RandomManager { 5489L }; /** Index of currently active generator. */ - private volatile int currentGenerator; + private volatile int currentGeneratorIndex; /** Best prediction for the latest incoming numbers. */ - private volatile long[] incomingPredictionNumbers; + private volatile NumberSequence incomingPredictionNumbers; /** - * Constructor initializing all random numbers generators. + * Constructor initializing all random number generators. */ - public RandomManager() { + RandomManager() { this.generators = new AtomicReferenceArray<>(0); initializeLinearCongruentialGenerators(); initializeMersenneTwisters(); - this.currentGenerator = 0; - incomingPredictionNumbers = new long[0]; + this.currentGeneratorIndex = 0; + incomingPredictionNumbers = new NumberSequence(); } /** * Returns human readable names of all generators. * @return all generator names */ - public String[] getGeneratorNames() { + String[] getGeneratorNames() { String[] names = new String[generators.length()]; for (int i = 0; i < generators.length(); i++) { @@ -265,28 +265,28 @@ public class RandomManager { /** * Resets the state of the current generator. */ - public void resetCurrentGenerator() { - generators.get(currentGenerator).reset(); + void resetCurrentGenerator() { + generators.get(currentGeneratorIndex).reset(); } /** * Resets the random manager including the states of all generators. */ - public void reset() { + void reset() { for (int i = 0; i < generators.length(); i++) { generators.get(i).reset(); } - currentGenerator = 0; - incomingPredictionNumbers = new long[0]; + currentGeneratorIndex = 0; + incomingPredictionNumbers = new NumberSequence(); } /** * Sets the currently active generator. - * @param number index of the currently active generator + * @param index index of the currently active generator */ - public void setCurrentGenerator(int number) { - if (number >= 0 && number < generators.length()) { - currentGenerator = number; + void setCurrentGeneratorIndex(int index) { + if (index >= 0 && index < generators.length()) { + currentGeneratorIndex = index; } } @@ -294,32 +294,32 @@ public class RandomManager { * Returns the index of the currently active generator. * @return index of the currently active generator */ - public int getCurrentGenerator() { - return currentGenerator; + int getCurrentGeneratorIndex() { + return currentGeneratorIndex; } /** * Returns the name of the currently active generator. * @return name of the currently active generator */ - public String getCurrentGeneratorName() { - return generators.get(currentGenerator).getName(); + String getCurrentGeneratorName() { + return generators.get(currentGeneratorIndex).getName(); } /** * Returns the parameter names of the currently active generator. * @return all parameter names of the currently active generator */ - public String[] getCurrentParameterNames() { - return generators.get(currentGenerator).getParameterNames(); + String[] getCurrentParameterNames() { + return generators.get(currentGeneratorIndex).getParameterNames(); } /** * Returns all parameter values of the currently active generator. * @return parameter values of the currently active generator */ - public long[] getCurrentParameters() { - return generators.get(currentGenerator).getParameters(); + long[] getCurrentParameters() { + return generators.get(currentGeneratorIndex).getParameters(); } /** @@ -327,19 +327,19 @@ public class RandomManager { * @param number number of values to predict * @return predictions */ - public long[] predict(int number) { - return generators.get(currentGenerator).peekNext(number); + NumberSequence predict(int number, NumberSequence.NumberType numberType) { + return generators.get(currentGeneratorIndex).peekNextOutputs(number, numberType); } /** - * Find prediction numbers of the currently active generator that match the input series and + * Find prediction numbers of the currently active generator that match the input sequence and * update the state and incomingPredictionNumbers accordingly. * @param incomingNumbers new input numbers * @param historyBuffer previous input numbers */ - public void findCurrentSeries(long[] incomingNumbers, HistoryBuffer historyBuffer) { + void findCurrentSequence(NumberSequence incomingNumbers, HistoryBuffer historyBuffer) { incomingPredictionNumbers = - generators.get(currentGenerator).findSeries(incomingNumbers, historyBuffer); + generators.get(currentGeneratorIndex).findSequence(incomingNumbers, historyBuffer); } /** @@ -349,56 +349,57 @@ public class RandomManager { * @param historyBuffer previous input numbers * @return index of the best matching generator */ - public int detectGenerator(long[] incomingNumbers, HistoryBuffer historyBuffer) { + int detectGenerator(NumberSequence incomingNumbers, HistoryBuffer historyBuffer) { // Check whether the current generator predicts the incoming numbers - long[] prediction = predict(incomingNumbers.length); - boolean anyFailure = false; - for (int i = 0; i < prediction.length; i++) { - if (prediction[i] != incomingNumbers[i]) { - anyFailure = true; - break; - } - } - if (!anyFailure) { + NumberSequence prediction = predict(incomingNumbers.length(), + incomingNumbers.getNumberType()); + if (prediction.equals(incomingNumbers)) { // Keep current generator - incomingPredictionNumbers = generators.get(currentGenerator).next( - incomingNumbers.length); - return currentGenerator; + incomingPredictionNumbers = generators.get(currentGeneratorIndex).nextOutputs( + incomingNumbers.length(), incomingNumbers.getNumberType()); + return currentGeneratorIndex; } // Evaluate prediction quality for all generators int bestScore = 0; - int bestGenerator = currentGenerator; + int bestGeneratorIndex = currentGeneratorIndex; for (int i = 0; i < generators.length(); i++) { - prediction = generators.get(i).findSeries(incomingNumbers, historyBuffer); - int score = 0; - for (int j = 0; j < prediction.length; j++) { - if (prediction[j] == incomingNumbers[j]) { - score++; - } + if (!generators.get(i).isActive()) { + continue; } + prediction = generators.get(i).findSequence(incomingNumbers, historyBuffer); + int score = prediction.countMatchesWith(incomingNumbers); if (score > bestScore) { bestScore = score; - bestGenerator = i; + bestGeneratorIndex = i; } - if (i == currentGenerator) { + if (i == currentGeneratorIndex) { if (score == bestScore) { // For equal score current generator is the default generator - bestGenerator = currentGenerator; + bestGeneratorIndex = currentGeneratorIndex; } incomingPredictionNumbers = prediction; } } - return bestGenerator; + return bestGeneratorIndex; } /** * Returns the best prediction for the latest incoming numbers. * @return prediction for latest incoming numbers */ - public long[] getIncomingPredictionNumbers() { + NumberSequence getIncomingPredictionNumbers() { return incomingPredictionNumbers; } + /** + * Deactivates all generators. + */ + void deactivateAll() { + for (int i = 0; i < generators.length(); i++) { + generators.get(i).setActive(false); + } + } + /** * Initializes all linear congruential generators. */ @@ -436,9 +437,10 @@ public class RandomManager { try { generators.set(this.generators.length() + i, new MersenneTwister( MT_NAMES[i], MT_WORD_SIZES[i], MT_STATE_SIZES[i], MT_SHIFT_SIZES[i], - MT_MASK_BITS[i], MT_TWIST_MASKS[i], MT_TEMPERING_US[i], MT_TEMPERING_DS[i], - MT_TEMPERING_SS[i], MT_TEMPERING_BS[i], MT_TEMPERING_TS[i], MT_TEMPERING_CS[i], - MT_TEMPERING_LS[i], MT_INITIALIZATION_MULTIPLIERS[i], MT_DEFAULT_SEEDS[i])); + MT_MASK_BITS[i], MT_TWIST_MASKS[i], MT_TEMPERING_US[i], + MT_TEMPERING_DS[i], MT_TEMPERING_SS[i], MT_TEMPERING_BS[i], + MT_TEMPERING_TS[i], MT_TEMPERING_CS[i], MT_TEMPERING_LS[i], + MT_INITIALIZATION_MULTIPLIERS[i], MT_DEFAULT_SEEDS[i])); } catch (OutOfMemoryError e) { // Not enough memory for Mersenne Twisters return; diff --git a/app/src/main/java/org/asnelt/derandom/RandomNumberGenerator.java b/app/src/main/java/org/asnelt/derandom/RandomNumberGenerator.java index a8f9df2..c2df712 100644 --- a/app/src/main/java/org/asnelt/derandom/RandomNumberGenerator.java +++ b/app/src/main/java/org/asnelt/derandom/RandomNumberGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,17 +19,20 @@ package org.asnelt.derandom; /** * This abstract class implements a random number generator. */ -public abstract class RandomNumberGenerator { - /** - * Resets the generator to its initial state. - */ - public abstract void reset(); +abstract class RandomNumberGenerator { + /** Human readable name of the generator. */ + private final String name; + /** Flag that signifies whether the generator is compatible with the input so far. */ + private volatile boolean active; /** - * Returns the name of the generator. - * @return name of the generator + * Constructor initializing all parameters. + * @param name name of the generator */ - public abstract String getName(); + RandomNumberGenerator(String name) { + this.name = name; + setActive(true); + } /** * Returns human readable names of all parameters. @@ -52,12 +55,13 @@ public abstract class RandomNumberGenerator { public abstract long[] peekNext(int number) throws IllegalArgumentException; /** - * Find prediction numbers that match the input series and update the state accordingly. + * Find prediction numbers that match the input sequence and update the state accordingly. * @param incomingNumbers new input numbers * @param historyBuffer previous input numbers - * @return predicted numbers that best match input series + * @return predicted numbers that best match input sequence */ - public abstract long[] findSeries(long[] incomingNumbers, HistoryBuffer historyBuffer); + public abstract NumberSequence findSequence(NumberSequence incomingNumbers, + HistoryBuffer historyBuffer); /** * Generates the next prediction and updates the state accordingly. @@ -65,13 +69,46 @@ public abstract class RandomNumberGenerator { */ public abstract long next(); + /** + * Resets the generator to its initial state. + */ + public void reset() { + setActive(true); + } + + /** + * Returns the name of the generator. + * @return name of the generator + */ + public String getName() { + return name; + } + + /** + * Sets the activity state of the generator. + * @param active the new activity state. + */ + void setActive(boolean active) { + this.active = active; + } + + /** + * Returns the activity state of the generator. The activity state signifies whether the + * generator is compatible with the input so far and whether the generator should be used in + * generator detection. + * @return the activity state of the generator + */ + boolean isActive() { + return active; + } + /** * Generates the following predictions and updates the state accordingly. * @param number number of values to predict * @return predicted values * @throws IllegalArgumentException if number is less than zero */ - public long[] next(int number) throws IllegalArgumentException { + synchronized long[] next(int number) throws IllegalArgumentException { if (number < 0) { throw new IllegalArgumentException(); } @@ -81,4 +118,60 @@ public abstract class RandomNumberGenerator { } return predictions; } + + /** + * Generates the following outputs and updates the state accordingly. + * @param number the number of outputs to predict + * @param numberType the number type of the outputs + * @return predicted outputs + * @throws IllegalArgumentException if number is less than zero + */ + NumberSequence nextOutputs(int number, NumberSequence.NumberType numberType) + throws IllegalArgumentException { + if (number < 0) { + throw new IllegalArgumentException(); + } + int requiredWordNumber = number * NumberSequence.getRequiredWordsPerNumber(numberType); + NumberSequence numberSequence = new NumberSequence(next(requiredWordNumber), + NumberSequence.NumberType.RAW); + numberSequence.formatNumbers(numberType, getWordSize()); + return numberSequence; + } + + /** + * Returns the following outputs without updating the state of the generator. + * @param number the number of outputs to predict + * @param numberType the number type of the outputs + * @return predicted outputs + * @throws IllegalArgumentException if number is less than zero + */ + NumberSequence peekNextOutputs(int number, NumberSequence.NumberType numberType) + throws IllegalArgumentException { + if (number < 0) { + throw new IllegalArgumentException(); + } + int requiredWordNumber = number * NumberSequence.getRequiredWordsPerNumber(numberType); + NumberSequence numberSequence = new NumberSequence(peekNext(requiredWordNumber), + NumberSequence.NumberType.RAW); + numberSequence.formatNumbers(numberType, getWordSize()); + return numberSequence; + } + + /** + * Returns the word size of the generator. + * @return the word size + */ + protected abstract int getWordSize(); + + /** + * Returns the state of the generator. + * @return the current state + */ + protected abstract long[] getState(); + + /** + * Sets the state of the generator. + * @param state the new state + */ + protected abstract void setState(long[] state); } diff --git a/app/src/main/java/org/asnelt/derandom/SettingsActivity.java b/app/src/main/java/org/asnelt/derandom/SettingsActivity.java index 874d7d4..9317413 100644 --- a/app/src/main/java/org/asnelt/derandom/SettingsActivity.java +++ b/app/src/main/java/org/asnelt/derandom/SettingsActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -174,7 +174,8 @@ public class SettingsActivity extends PreferenceActivity } numberPreference.setText(defaultValue); String errorMessage = getResources().getString(R.string.number_error_message); - Toast.makeText(SettingsActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), errorMessage, + Toast.LENGTH_SHORT).show(); } String summary = numberPreference.getText(); if (key.equals(SettingsActivity.KEY_PREF_SOCKET_PORT)) { diff --git a/app/src/main/res/drawable-hdpi/ic_action_discard.png b/app/src/main/res/drawable-hdpi/ic_action_discard.png index 703b31f8027859b5810937a5c2da2b97428c68ed..4a9f769475ae98c44086a5498057c799cdc1eb2e 100644 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0y~yU{C>J4i*Lm25-&)VFm_<3{Mxw5Rc=@2@<R#4gY2S zYyUG>IN0@H=85Fy$M#k?9Jd5IRCluSI-gN^bu5fI(PNIIOG99@8?TgU#+3yNQ!m&z zJ^Q12Zb2~%ua)y0N3;L?ADBM7T(6zfT|e{xPYdpUfBs)ud{*)k1H;d~3-z`ibFXD! PU|{fc^>bP0l+XkK0ew8) literal 450 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIk|nMYCBgY=CFO}lsSJ)O z`AMk?p1FzXsX?iUDV2pMQ*9U+7*jl5978H@y@|Zb+hicH>{vKk17it;OoM3&gX{s$ zn(M4{+89$9BpKKe81{Jgi8n{Rj*Wg)dvfW)2VeJnt26Z!;c8V<;aR>ZQKBef({^d8 zV0TZ39{ohi=smX>I%Ti#<34sc!f=0j>(U9?a%y&mrEeAGt2iZ`I)Cu!q?-a2f@dm2 zA~||o;`JP^rRdl#pBDe#Au8&<g6W;8$E^&hvmY-ndC8zs%&0Wgq;YmeqEVW#RxjJZ zcp;0%FHGCMEV&&mrlt8=WQvk;`+}oBA;HywmPdJt|M}}iv6^h-e^s84(J0hxcipRJ z_S?i@o0Dx%ZU@WeXX`YC7pbpb>*AHtWtpnDmf29?1xwD$8lenUjxDd8yb|14mdvuW zXNfs(&pB6$*-Js8(XNEyzgL6t1nJU+2Y6$}n+_aJZJOoayX(#?_QN*a%88!AGta(X z{8*x^EquS2tMb#?=bf#-$9r?NItjANG4U|$ew0>pHOyoM0|Nttr>mdKI;Vst0L=)i Avj6}9 diff --git a/app/src/main/res/drawable-hdpi/ic_action_refresh.png b/app/src/main/res/drawable-hdpi/ic_action_refresh.png index dae27903e9ba3415808d48e6ac20afd6de64907b..67b7f900a7461b40319d14d3e3171508f3f4603f 100644 GIT binary patch literal 528 zcmeAS@N?(olHy`uVBq!ia0y~yU{C>J4mJh`hKCF@W-u@?uqAoByD)&kPv_nB3=9mM z1s;*b3=9k&VC;4>+YTgHR^XTp(hJ5F?9%-V42-iqT^vIq4!@nY+l$#zr0slHuh#{W zq`X!E#~QY&Q<P?%I&{fNaPb6pkGIktFLQEDTUqoUXnX82U8E4v(KXeu&P>`u@5v_P zWRH#ZIy{Hx{jd4+=A6t#kBLfO7dDs#FlagO{`fL~*8XsYpxjs9>l}m}Bv)LHEKT6| zU`b?%Iq;;hU(cbbie1dK$5bGIMXJ$F{Q#Q*Zx4fE0^1FibIkASnBA)mn@RE(b3fU& zBAP)i>S!9*<S)wkub4X8SIlRa7sE7l>Ty{Eu}%A2xMv<nVX6;4C)%TVVcV+pyIKrc zt-`*()=Rv_$h+<1xm90NryE2HN(xQ9dx5hgaUK7`8K2p7O)ji<(`%S_)#~cW72*!x zvutnkJBV(L+nUQVXJvlT@goObCGb?7)t+x<ke8KGTKYq7@qyk?H@A1*{8rTaxm=`D z*<iy=hnaoy#Rs;o`pr0PZNikwiRYMa85ppxVGNaDu##=VQx>%eQ%-XH=2~OX8W@n+ z_DBB#gGbxThmuW8T~+jzyp@bJl_c-Kc6C|j@+8EC&-Q%vpXn(|u3ae$<9R=<{qWRn RM>{BHJzf1=);T3K0RUe$%)0;p literal 663 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIk|nMYCBgY=CFO}lsSJ)O z`AMk?p1FzXsX?iUDV2pMQ*9U+m@GYA978H@y@{~(J8U4*w|b2wgUkZkhMUo4r{eem z$`1MMcK-f_;Vs|Y%<`f}nFcw9H%}#PC!|`Q-28Zk+b4C~$Re5N)s^<&sslv?1Oy&% zEx(+3Bm0xW`(^Jp@^z>f@B|9I4CD+lIQFT_e*VK>(Gj0thBB(uG8?%*^1ab4)9_-p zQ{02N<tgf13LhoCk~X$(I3<3f)#SN5qr&;b2o3r2M)80I?^v1WN#g51g=~Jpq4X!> z-{cr!aVv>PqcWd5_DzlU5m)xuH$J>Mb!U?M6W4%s9XFFNeW<z7$uvV(?$!^THyaP+ zeAU;Rl4Ta(74l+2jk#V+-RY?>*E0SLQTi|aAoPT<NP-K~%tZ$TPxzmI$aRS0iGE^5 z=(1kE*?NWwYa~}&{);&gEc#&96v?lijTbGo9XcENQ(sBNtoRb5{3l}nxzt|KhO_Hz zLQ<t)nX?C;ceFgwqou+g;QxD3s6e&a`rT)y=5C&l-!mubKykX#MVUF3^K}d^ml*w8 z*l<N*LScqg@r<MGo4U>{T)jlD@xZ-=_`F$+xm{I$G22ZP_ODoBE@ix+m({Fg*VK@e z3vVx(<>f14TRug*yO(vA*<GQN#VYGh|JMAvPt>~a=&?18PhYM%CK_IvD=DsL(w^uS zG0R(7{fK|3<NJMT+d>YoSRU*Bxb>(jkH`$S+^Qu?5!bK0Yt!cDFh8O4Y0&}wjoIdI z=AImdn-(v-xlZJVa(v(vm9F#LmzH`ql!X~~Ze}R1uy5SEsG*^u;oA?f1O~;q?**61 TPwHb}U|{fc^>bP0l+XkKIJqG4 diff --git a/app/src/main/res/drawable-mdpi/ic_action_discard.png b/app/src/main/res/drawable-mdpi/ic_action_discard.png index 248fb09cd0c918955323e790c4c3250997c99819..e2f5f35558db0965392b15b5d8950261a8c92aab 100644 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4i*Lm2CurW#S9D#R-P`7Ar_~T6D0mT*nj9h z`~SoT2kTjDygo9#%1}7|cgZaKRf!?Ydc5m5tg3H}ZvOv&`ixWmfAf8^;b(AGpLFn5 TddMdR1_lOCS3j3^P6<r_D@Q8a literal 324 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}k|nMYCBgY=CFO}lsSJ)O z`AMk?p1FzXsX?iUDV2pMQ*9U+7@m2$IEGZ*N=lGmRhBiFwCdsi|NpZbPBFHBDROf= zc7Dl><4$dhyBlvv^#9R`vSwhrH1S4OK>I|V85#u#(swOkOqlbTNmD*!!)oS@DvY}4 z8-J(?%x8V1+j5D;knaZnl8TeOstcH9G<%uixSVE8m&$yg!^&sjJ++8o*7u8-8$^Vp zMc$vkaGAm2M}FYaElgGiriWE^@HQH&xUuh8z|_{jxs_WfIqN#B4-Yd>TMzRI%`*Go z%ZyJ_mPaqnOt`{$rG&v!^wQ15sl3}A44JoHoqSox+--H&#Mw#b8$>Tewgnn+c`z_A bvN4?aVSW6ZU0DwU0|SGntDnm{r-UW|DGhl* diff --git a/app/src/main/res/drawable-mdpi/ic_action_refresh.png b/app/src/main/res/drawable-mdpi/ic_action_refresh.png index 94ab6f4c5dd8f3a082b2a84d6e08f2564a589d94..dbaf3677276091cc8733877bbcef67f3550f6c86 100644 GIT binary patch literal 360 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo*pj^6T^PXNr}OT51_lPs z0*}aI1_lNXFm^kcZ3hx8D{xE)=>_8opQXzf7#Kc#x;Tb-9DjSo+l$#zr1hbKgyTuK zo<+?a2bb=c8P}N4>Yle``$dZq1wPrI3`bu)f5PU}{4#pcBL()ZS$|6TYInc-QpNqZ zI<X_!q@wHU1A*7N+q4YdJ(hXD@0#?v4Lw|5XI>jr9_2h1ahRccM-Rh}nQCdwdQJNe zf0M}*@83LsisAhOEc!QIC9M8-#)#4V;Jj(G(wS!^9AEnEW<z@d`%3w13zyC}(B$3G zTl*uLvGmRFNa2{H5&T94zh<%7bJbm1*H_uTi9fgH_nEDE@^ivPSKsK|=(Bo}o4u@o y@TJf)`GadjW^Qk~Rr<H*b;iZB`O^Ad(tok23P17B{r0#E6k?vPelF{r5}E+kRFM?` literal 508 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}k|nMYCBgY=CFO}lsSJ)O z`AMk?p1FzXsX?iUDV2pMQ*9U+80UGqIEGZ*dJ}%Ju-Slz*SAixf#(2|&H;v!2E7Y> z7Z{oTr#5`qf09Ax0^b29IR(iBf0s8^@4dWpT5xhr&6ZuCZ?^Z&)A2t4sciG6od0_m z%MGXUAKnmumZ4$)-Kg!KLuLhq2WnpkGFyAW>Wltko4>&-o|o9A9lGRJRvS3%e3G%a z@z%z3=NaDo;&>WkQnrgxV!nUt#`%m)KVF;*zq~f0deO8<o6mOFnc3Ow-tACK@VvR1 zwdSATg+8xWUTptd9(YAWKYo+@$mi&hM2F*LSA>4tEcDsQ@!$l*Mdl?Y=?}s=Wf-Ii zH);q^%c+TK?mD}oUoA*r*_oqS6DGZnY}mHU?j*z32WrL_woRQ<=9I9NTWUl6TdOt^ zz4OP`v2#s3$EEqYR#LRj=cmfjxsSik>OEh){_)}uwTsf*A8vI_i!E?WYPuaHa;3)L z#2P+Lk14IM?R^TSBurhJk?EwZl>0MGFoE^4NzmCcQFHU8BfmM;u}vu9{$_S<cD6%* z%)E6e0k6vgXGy+Zw#a14zIu_x9WP3snD=+D&{r;c{`7yLRPa6)2?nz_{WpCVS#dKk PFfe$!`njxgN@xNA$2;KP diff --git a/app/src/main/res/drawable-xhdpi/ic_action_discard.png b/app/src/main/res/drawable-xhdpi/ic_action_discard.png index 9eeeed124dbc2e6c32b179a48cb449bec2b2aeb2..388b5b060af924493b057a63216fe7db75d4435a 100644 GIT binary patch literal 151 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4i*LmhQHi~JPZsBah@)YArXh)UO33vV8Gyf z(NDSWW^Wgtn$uqQ4>~ulY+LZh-a$@<Ws#!ht93WZ|M4z7#c+%9|BBzX3{Viy$d_&Z x`#=`sN%pk2M?cS2litzc`tzsE(p8m<9{EW<aaKM1bjf0nVoz5;mvv4FO#s%+HZuSK literal 543 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE<t`_ZS!$BuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFdp=DaSW-r^(N|KV2cA!s%*Wc1J?nj5Aoa=m~tNI zJkW9A-oUV)k^2MJfqKmc8Vp<u8uqSflRU=%<=rO56V=9&`(~)<&wdkqLxqt61R^x% zv<bieJ4^0o<NCc8&tKgXb~q{irNZ{n)kz!`J+k*_KD;_J%fd3;Z<Vt2>Q}Ni*QPn# zV!5(q&Fc7DuA<kX7F;S@v6snd>g9>DidXxNyLuQrW~n~icZ2D7Wg!1N)9nGteGGoT z=Dlfqqf~V9)&|!}dp##SWSJCyG1>IunHvI}vsZHdm=ki!s{Ctw>jkCl2P1_p_({}f zH#V?uddqS$|K@gH*#k`KIn75Mm^)5oZ0qvxn)~yy!Nf_Mcb%?#(fo)%$U^7+hV$i8 z_gNVlv+N6xCjUO!tY4ho*r>qeU4HzpR^g%7w^b)p-`sv_C6k64<K}<{0Vf5Ac?`a; zH=<M)1gb1><ZfUPXi)4>>=0vMR7hCqykWCP!!v<37nu|oSY{|M2=2LkPvFX*65W^F zx(rMyfm%mmqSPK(c2-|rog`oMS;cM2z25~7KU)3#7pWEcm0zmv{Uc`6Z&goE_y6_3 q^r*qB{w|;N?g&l>FqqS@mw}n#aes|a)-{7}kdUXVpUXO@geCy&<mXoa diff --git a/app/src/main/res/drawable-xhdpi/ic_action_refresh.png b/app/src/main/res/drawable-xhdpi/ic_action_refresh.png index ab4ab9da697b8c68c0c7ba498dc3bb3dc3da9c2b..393118decca6b3f8279279870a3ea45d08049926 100644 GIT binary patch literal 665 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIY)RhkE(~Ds(|LD20|NtR zfk$L90|SEx7`vU!wgU;46*#7Y^n&rZcRk?@3`{DXE{-7?_ukIfn<W}3(t5pPXV*O! zM+3J2PPY03hr%~pK4RC@>l!7mxMWjDucpWqn}*g<IR}m<7evHvaUWc9aP~X5?v4+U zXBr-tshvA-SFC%Eefx|@YxZ8dlQ_S4n@FdN(!>sC2l*Y$Hq2&?j2q_vWji4KK+}N# zi*}Y?X<`jq1na)wxqEfad~tln=p<#&rNeBWC9(YtGk-(e#GeU_4Vn*1xgSsCeXzw; zUWK86_lBO~CSDuHJjU>b#|JD5L}#3~n>}~Z*65qJ>_3>*FFW!_Jb}?7#&I$G9rm)- ze@y2+>z`02bXv;bp2K^Q4fj$To-+LveGtHXR_=pd1;YcS`tsRZ&7K+GRsGi(-VnO@ zx8w|t+<U4Fnc_dwO2l^=ycbQlmr=LGY5PZm_o5GWUaYw@wQ@bfw$gKDsrR%S?iT26 z6?-dmi8*}Ut=VB7QC~m)Z`5LJdsdlsW!Y=C9|`l)-Ew#3HGF@zGf-Sg@}Bff#?q=K z$3NMa+qT^87g8;KH&bf&ff>_x{7yXgV!lqrZ|!;c>L&`VF8eizyH)egDB+$U%2l|~ zv%#HpSNFPSo0IN^pW=Lw+IWvG?aU*|Lyv>(+3x86c&0cvVh(STUHXD|5l<xw=9R4Z zB=93-QyKRTnJYDVE#DJf>lUmN{=8ElZt+pqJ-=!XEBLnUcE0qkcwz3}BZeOFmvTML zRbr=`=)P=OGJg`=q<vnurhM_bR5eNEaN1O*i5@CKKhNs>K01GU(XYV$pd{w$>gTe~ HDWM4fTCE=y literal 895 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE<t`_ZS!$BuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFe`exIEGZ*dK3M=aJ2zXc~wnngMq_62A&Ii6L==@ zeXwHq8~1Ck<bp^A<_U~O4#p0~43Z6upO#n`_V3#ExSpkBt+i|}KhOU8s*#)Su#4@m zabZ$ooY>&uprRmT;JY(M&;Q^#nTGhrI@U}^2AlTNv->wrlCCRZo9=$WenIgC){I*_ zmonNm&U4)F*uVMa8x;xm`M+QM)7zCV!gAoa;sMhaT@I^R-k*K>T-CNeC1gQ0L!|Hz z|Gphpo=T_NGU-Pt%uDeXJHlMYSk$+8BLhRf@xSY(X)H?0!fVqim@=-}Tcz1>dfGI) z-@lXb!*a1gyKz|Xh3pei1wB3MxSOvpZx&ME^x30uB3Am0%6FL^qRF1?N_@qd1s&4X zGu>yp&-lU3+US4(kHzJTq7^3Z!;ki??$CX>X!f(T=9H!n6I&1cxo~B^?19-j^FPlP zc6hcktU+J*xGAfBglWVHrkTt?w(3?)ub3FIM`!w)uRHeawPp#vvQ}l<38sw4LF_l! z9B01F_ffJH|0>P-qi`p~+VhKE^h~|2*dn{o{p9`f;JXoqstzxH2hXjIXYmo+Bgi<{ z<L~MJ%?b_^zg`ViJFru#B(~jl+GEY<i`?hVUDR^7U8L#9maB#5A6Pr9i4~}Yomc+y zdrdjhiY<5hN|c^Y;!G}o&(!6$fv<@{?S89WtJ485mbmRkbE~RtCaYxJy7}^ouEVz6 zxVyHDTa33(30Z#YX6B=p&Mu|RIlFD%&Ukxm*~Xih`?`)~to$~St3JwZd-;o3r?x#Q z{H8s*v1Z4Cn?g02nT4v8-^)q&zie)(QhBge=hd~V3qtQVOxwL(+w*+dgue_`mFp_+ z?^gWqt>f&hmC_rQzMZy*pGk^2+;I`#te=0o&iswK{Wo<>`x1*vC)R@8WooZ}JPDr` zVE04Jxuk8)Wk0@*cR$XBM{kxl;`miy?lW(XY0|GQ#@zMqXIg%HZ~a(uzR5*F{pTz@ zjd$BK3Cm{08T}QO|8wf1+?1SO1}p72CdkFS*>WwkaUJi3<NF-G2hI%jV4C+}UR3By zZH{XqbF{=3-Ff8^Jw;CC-}wL)1t9^>4we?3|11&=i&V4{`wI64GB7YOc)I$ztaD0e F0st6?k--1} diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_discard.png b/app/src/main/res/drawable-xxhdpi/ic_action_discard.png index cb1260a4c68d931cb7dbe80341355c1c2438bd9a..3fcdfdb55ebcba8d2fff8be03ea3518c137e3464 100644 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84i*LmhW}5h&oD4Bbb7ithGg7(d+98%g9C%h zL)(kY;=Ej?dYu7{T1RqA6jIuMRIYE*pUm0V(IK<)(we<#q5hBVcv~IW*|mh5iG@R; zcERzz>;ei74GfGRE)wHC(^|3fkC?xT1$=3Y;$HEsvB>x29LLBHOM{Y?mUG;jvN~}2 l(?3&1L!+-ND*XzV=AAFjZ++R+o`HdZ!PC{xWt~$(69E2$L(c#J literal 765 zcmeAS@N?(olHy`uVBq!ia0y~yU`POA4mJh`hDS5XEf^RWBuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFwOULaSW-r^=9rx-@^t1ZNf*g*%vUEG#WUtH!wyt z=v`oxU|@T}BFk{^HsdT-BL^!7HjRCcLY4`cv>vguzOU~3jFI(u-SczLZ@5omWPpMO z@uIVHt!4OJ`M0n9WRoxO!0PaMpU-LQcFX0dAB&d~^kR9Tci@zXYyb8;(u{I~3wAT? zz9(;T{=KQ)Yu-cyo$V99e136!{g1!X=RIO)vM`=`&hg5Q4Qn&!B~@$)i473;cJE~5 za1iKYo4vp{sL(#^6T|Tht-0+S3xtknYft!dXU2iJUO$H9H`gu{ovk`o@n5-T%hLCy zi|;Ic$Zfsp`Ki54K8e@&UF$3ISN<1z(BuE<*&Ahk^?p!1pyl*<zKa2qPLg=Sbk{Q% zx+|VAsw8nRbaHR#zhT$>{jR&fw3O_tiJXqoMt@^?=IT~^9?8*D@SIUIeQ$TdmHoF< z)^z^k{h_ElPdl<LBXhdawteOPEb%>?n|$8RzQ|=X#bD{>Qy%M`^TRLhGt91fwn^>V z*_|e{Vy`Pk2W35rW>|cD$CTck^VVhuK7W^U-|XbEhRv-z%bqFbzP%i7y)pIGnPYw% zk3QGiHGwI{tLc|t!>$^CHwNAxA`M4(9QYx&pyz;E>M14$CItos2FDo+XQUgrnOGbc zSU&vuzKSC!XsM0^&+_nDQ&<^T92gWh`b{{p_2!7I>ffP|#K3gm1j~h*gk*a*Mw9$0 zYmbABU;-K9BXEpWkXe91xW}L2?Ci~_p0I9sUSwJ)uHigKlCQ|`#x%VNzXXj{k4<OX z$nkNn(hbMl<#jDF3%M#g4ql(Re3!r~77O7=ioaLMKU`gQWTRfoPCKXirNQB=4qWRi zc<nxCy7msU&1>gwyYg+lPssJIAFd0$9p#{Dh=JjO^?W8C20hpFocS$h6+yzDu6{1- HoD!M<`@uqF diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png index 44ee117ee91541ed71f27685dde0b03b51b89f7f..61da9e5815577183035f0c374fff6871564008bb 100644 GIT binary patch literal 982 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84mJh`hS0a0-5D4d*pj^6T^PXNr}OT51_lPs z0*}aIkTNiKJDF_<5-cllOa}{q%s5y1ft7)QInmR_F{I+w+u7C`B91cc^~J}$nj}o^ z96BE<a|P-&>3`sLW68}86)|yKJw@nf(3Nw3Z!X=8U`<MVsH~W<TBsvISwrY)%>p<3 z?%PT&XYRdADNo~nuKm65@Be@I-`#!l=A73=4;7(K7bOt&v6nMY<i(Vdh)Z&wcPE*7 zyqdB_<>#qzPkGh7Q@=!A;`a3KF_agJC_f=ltn6|=@=z%Q^8twu)BpTooyR=AF;%aK z!Kr$N{*9;yW*@FPcJ*2na7Rpa_gl)s*xh(=$;?eZm||FOZ91{CM<K!Z!zLHQVo7#0 zlgYuHjt5>oIq@q^>V@~@$4V<M1oG_AcsXy0tK`0aVV63VET0tTS*5;mUd=VVmEM=k zJlR#gdPYr8oWEan?({8zm%dg?o2i{Rq5B}HZLtkA4|8?n8OKJc0-lPN+Z+<#)Y;~E z&GJtcd7-x;OyA(5QB#BNlK;>Dc+^$7UR%R>-AHE2hNcXjr<sedvM<<v<d=G5eyEtq z4VQCa{F3j&g9J9P{dRp&p0K^0$9d}CPR8<}B9UM7gQv*kH#I!H@jPsC=+wu-7aErS z<6-4_ntJl1X2zMI`W>f=_HZy+ZHzzrZuKUa_@;9aS^g7~e+fACbuXCZ9C!PJZBmcs z9}Y(6O+GfY50p-nef+-WMZv<SJKE-LnX`AdQ@S?ubmJ3chyTRBNIP}FHDGsYwe9t| zC>C+Y|Dt+LIq6>2>-KKp&}&+zd;5BO-ZGvT7T>6C`TTc%BzJIBCEwg<Ut*m4Kq=$S zroHRetaS4h%-S?d`F^4Iw)g`<@rq4%-kq3{dt}nvjpqdp=FKoYxz+gT8@t!W9v7n5 zP4h8*(5iabWJAis&`+#fRhR4L*@Opuy7Oi3a-sg(v{Xx`KR%Y#R(rU;nnE8QOyvJy zsp?)YVClKzLjOE|rujjy?3p*|9MudB>Nv{H_F{QqZt`Q}3!)+C)*P^#zW)uwJhPRi z%re|>Q}aF;i3dnN(D-0*xbX+`9PL%>we${VPu}8m$;;Dr5}%jVl4Fzhr2Uy#a!}&) zf)o3=S_`@@_A{tEeV-pX>3!+};RDMq)IOj3Bw4Z2cICxwAC5e9nfg)dkog0>4OS)f zu3cJ>Ql}M46|cLazj;T{(d!GdvU<|ytN5-oJk-1Tqt%t)0+q_3v<^w~kEYj$YP-ku SKP##N<xfvnKbLh*2~7Y~6}9UC literal 1239 zcmeAS@N?(olHy`uVBq!ia0y~yU`POA4mJh`hDS5XEf^RWBuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztuvB`wIEGZ*dK2qdcFRCOwDMk2!xM&>hBw~d7giTD zSTNjV{$0>e(2x>;qy5ce?jH;V415!^qJCcWJvd3XH)ZDk`<I*9f};PQ?@3GX6l7vR z0vXJ9_V&jU6lH#T-`tj<B=b}L%7M1}PnVl&<e9!GDL(bv#}l&n`}SALzslqHo=>%& zdQbkv*6XiRO_TpUI^D1S>HmrKehi++JofRVN>Am;=MqZPzr9=Kb3k*=p*4LSf!qsp zY^T0mm~wNS<F!9po_D1t)XOqyJX+pW5x=0|>#9AES2}&@6-l`4$*A)2%FCVUk6t&0 zHmDTz#2d6pg(vLYuQK7P{*?z(%xrT79VYqDjVvnHQu|^v`G3yCOFO+(>!1IaUQ}E; zci}40wpBNmZE&w#TX=;jV#55@KB@Yi7Lh3tCMWu5cPXx~m0!!id-8n9w~3a!+V|~h zcf8Hw5#?qV`DfO`4>ND~SU+){uJz(tRl}xT)1~sxPs+c1W?8dU^IEBAUm98!e~R3e zx3qfEQ4@B@AhJGd#&h|dYfc;f%*iZ1bfJ*THrV--y6<M`v$j35(RKD}{4=EQ7)-QF zSE(2DRxEqG<1@4K>FOsHCk}f2GngijB2;DOW4K>q$0p-@)3Sr^d(W$QR3r7wgg2Ds z&y}Y?H#rM4ZfH>uejVRjWXHHK)9!wu-L2hOj8_he#y;E3c(+Dwf%Yz=bvs2k5~eMC z>tGrzmNMgS&2x=i4;a&~&2OxUV=OVAccR9rnjzy`Fni1)L6i8l?wt#Ep9<W!_~r6h z6*^h-j}^P=ZCb$J%kW)Ru{zf9Uf?3b>|~BtUOz0ket&g6c&tIM;caKcp%mTN?%&&U z#S#wvy`VHnKvaz-mh)%%tn>T_S#}7$aJs<B^ua0ot$81lZ9#VYYmV-laTN{o{!Gkc z*f#TRD?`-_mJPFVS{SZg^JkrrxA3MM(+5S<AAB|q7k`y9y~&yX&9UM1g4VQMmI|*w zw@hf8x4mvwU7g{Hx;5qgYI0Vew{}Q|M1;kM<hT8JULn<2x=ibf4)4BKDGO)z$4<&W z^tVJWbe{3DzpUQPXO7>zmnfgy@cQH~VdbAI4Q}7xP*QsK23w-vr<04Gv&G-t9h36r z@tTgs$)AeVPS2OVclgxPt+gBl8QMR2&uK^P{I<{KNTWO3r`PlDI31t!Hn#jsJHxgk zYh#Yxxny>0%dz!=chYa(eeBF2Jw0|h|Mg;%trM>KbQfPQyk9Qs(0XHYYsLFhOy7Ce z+Y8k%n<6h&^}eULbo=_xO?EPsLV}YzFI~H&WHVLk-J^u-j}9zbc0-dtB{oWaGK1a6 zQ=*@pIS&}h9yE<xYq4qWg{cKR+3g))<n2!OujG5M?#lFq-wf2{tRrV;uJB3KlDM8Y zZ|Cyf)h65DCLaiyV}I(weiQ$tz5Gq@6T(mBZOgs6RX(WUluWd!pK@aTzvqE}Z1?Re zwKl8eba1h&J!zKZyKd#*6Wf>Gsf!5I?pPFeKrAv;W$Hhj6SHS=Ff(y5d}8Lj!2a{0 kt3)F!QpwP;;D_n~hS^_UOfQNrUICKuboFyt=akR{0I51fa{vGU diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_discard.png b/app/src/main/res/drawable-xxxhdpi/ic_action_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..8d322aa9baba09c58b0058760fb7d53a392b63a1 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0y~yU`POA4i*Lm29JsRH#0CW?DBMR45_&F_R4AALk0q_ zf$tYcx^%x(bX+3kBI;NoWF}d*=4XnI@Z9Kw)|YN9^_ZlR*}AB^{Pn-;y6zVK=iA+W zB-PbAh43*jFfc68ZCGN}F9~Ae#|sn>D3|`9!@pxW)3TZamZBG)Gxu#=ayI@^z^86Q zUV|x8NA1>We%9Zpy;LkQ=I4ZI606SE?`;jfll1TJ;~vdlTHEzKCvDj|FX<`6N{iV^ Q79h(!UHx3vIVCg!0ImO2jsO4v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_refresh.png b/app/src/main/res/drawable-xxxhdpi/ic_action_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..ce518e1add7e2349fd5bdaeff0510aa74c926569 GIT binary patch literal 1326 zcmeAS@N?(olHy`uVBq!ia0y~yU`POA4mJh`hDS5XEf^RW*pj^6T^PXNr}OT51_lPs z0*}aI1_lNXFm^kcZ3hx8D{xE)=>_A8+c&>3FtF_Oba4!+xb=39e}=59%<=t`og8j$ z*>{7jDwF?cm#rpOU|?YWqKZ`st^w=imi*hP@u>T)>mplGme#00<sX)@e|=!T#`Z>& z!ojd)(XSI0dEN@SRi&XK&;PsSL(j~+wV%t<?)|P+n11Hdx8J|(%BRn~J98q>VFOeo zQ4*8GXxG5$S?+O5`DK;oZq=<;_0rbuv6D(1FHKq!>1pp}rTTJ4ao)!MTer^boSsyD zrT<VK6Cb1T0j{r+jQg0EHA*vO=P)1nrj_EDd|>l|m2y1a4Vi5?bKV4I|GqEZ^ER5} z@p`sp4aK!4`({@@s7zQR{&_PWL&1UM15VAy%8SKrgkS%Y!^}{|cwAZP{@L^ceXE|O zOEP>=_z<3R{Ca}mj`dwXBUv3jo~Sh4c3Lj1p)B&VGs}e&77P+oOF}LkHmy&JyEJ{$ zzQBve|8M=g|Mc8%zLU0j-ddWpreOAx-73A;ZSG4?nm6G~(~^~?yd`>XH;Nc=)dW4x zvSF8DQ)@is%A|Z??t#DuNxh$^zv4Uo?Rt6>^S!kZKV&|b*nYdZyQGJiy^-6r>4a57 zxYm`T)}$l5R`yq^HEeHJV(7Z(7g@)YmlJUNugKnc2YDJ6&5!?9_MhQ~jN@az2e%u! z8y@Y9FPreyYJ=jUYlaU#J90nxIO$e=_u8$CzaKcj!mvdnuH@$rgJ)Csd*$yI{~XMd zu&U)*q>|0&>lbcq{^`l;@X^Nh{l)55wrcCw8N5k#S&TNpPVY;YteR)ek7C%j@OR`d zixXNa4Ojd;$>Q*l_xkbR=CnF5Mww49VpJaNo*Qz;U$a57|J>o`j5;AkhT9+fWoOE` zd=BPJSnvLL{*{XP^P7JvGD&a-yq5F-Eq-pN+j^0%^IgHd^ET@#GVM5bxo*+PrXnNz zkb~;B<uCVcG+pM?VV!X3a-CY)L``8OhWjtdG@-0Nr@A+G`^*$zl&}?0Vz@6FSIPFt zKv1c{d)~LOH%h^3E)1XNealW3pJ}AX^r6(amU-(!H5Z1>^S){5KPpmmVOVba_WO|v zNpq74A6~zZdGV94;GC|<uY1Kels9s(<@zw~`J!J|M=G6okJx?P@?QCo`pH!cw^ePQ zSiV_&!z!HH@89B@qg8y<R-~8b%)Ps4^89H*oBG85t$cmld9MDG-3!cZ-`-Z;dqe#C zAtT+FQs)ible+)#r<j-)&XszncX9D-=5<|bzy9Yskyi9h>ehCKbgvip<)dF$xu%%f z)=qsFcuM!m58Ioo>q8B9>Rwo}X;N{1&fZk+Q>mSoG>%$5GOCfeeOs&b=?0H{?GK@z zz1$+rn`;!_rmmWN;JoVAwG8}6zHQE1xKj6u+<TjE)3jey>22%ZD>CO)7^`lVs7<JO zdsX|L_@%=6oBMbV=pGP!@LuY9{d_hnj^+)qXE!Z5J87HOEzOr{qPJu2-TPTDdvEfW z6~%cwIqRhwYs_-20-pDsX1jEEQ%<K0TU4WyNABf)$p_Be*k>Z^#C1FDT)7F)JLTmr z0)}RJw*DJiGQHpYn%tPSP0x<^jrn6+fxORo4e|}s8>5r2ZxuUf|NBFJy#1o2wr{DP x#gpnLf9YN_d6J*S^M4(&&506isOa!n`>%@^+|2lQ$^lgFdAj<!taD0e0sy`uYHk1k literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..34cd486ed48ea0209db08af870c1d411b3c11f84 GIT binary patch literal 39782 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%0AIl^Qr4Kln8wNqHBwqQGn>ARo7gkrl?C^ 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ℜ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 RGB7YOc)I$ztaD0e0ssdheA)m2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_display_parameters.xml b/app/src/main/res/layout/activity_display_parameters.xml index 67cdd50..bf8cb5c 100644 --- a/app/src/main/res/layout/activity_display_parameters.xml +++ b/app/src/main/res/layout/activity_display_parameters.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6c78b03..7515133 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ limitations under the License. android:id="@+id/view_horizontal_center" android:layout_width="1dp" android:layout_height="match_parent" - android:layout_centerInParent="true" - android:background="@android:color/darker_gray" /> + android:background="@color/separator" + android:layout_centerHorizontal="true" /> <TextView android:id="@+id/label_input" @@ -44,6 +44,7 @@ limitations under the License. android:layout_toStartOf="@+id/view_horizontal_center" android:layout_marginRight="16dp" android:layout_marginEnd="16dp" + android:textColor="@color/accent" android:textStyle="bold" android:textSize="14sp" android:text="@string/input" /> @@ -59,6 +60,7 @@ limitations under the License. android:layout_toEndOf="@+id/view_horizontal_center" android:layout_marginLeft="16dp" android:layout_marginStart="16dp" + android:textColor="@color/accent" android:textStyle="bold" android:textSize="14sp" android:text="@string/prediction" /> @@ -130,7 +132,6 @@ limitations under the License. android:id="@+id/text_input" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignRight="@+id/view_horizontal_center" @@ -140,17 +141,17 @@ limitations under the License. android:layout_marginEnd="16dp" android:scrollbars="vertical" android:gravity="top" - android:hint="@string/edit_message" + android:hint="@string/edit_hint" android:inputType="textMultiLine" - android:textSize="14sp" /> + android:textSize="14sp" + android:layout_alignParentBottom="true" /> - <TextView + <org.asnelt.derandom.NumberSequenceView android:id="@+id/text_prediction" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_alignLeft="@+id/view_horizontal_center" android:layout_alignStart="@+id/view_horizontal_center" - android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_below="@+id/view_vertical_center" @@ -159,20 +160,17 @@ limitations under the License. android:scrollbars="vertical" android:gravity="top" android:textSize="14sp" - android:freezesText="true" /> + android:freezesText="true" + android:textIsSelectable="true" + android:layout_alignParentBottom="true" /> <ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignRight="@+id/view_horizontal_center" - android:layout_alignEnd="@+id/view_horizontal_center" - android:layout_alignParentBottom="true" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:layout_below="@+id/view_vertical_center" - android:layout_marginRight="16dp" - android:layout_marginEnd="16dp" + android:layout_alignBottom="@+id/text_input" + android:layout_toLeftOf="@+id/view_horizontal_center" + android:layout_toStartOf="@+id/view_horizontal_center" android:visibility="gone" style="@android:style/Widget.ProgressBar" /> diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 59af380..b7445fc 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml index 0d3a10f..c1b4982 100644 --- a/app/src/main/res/layout/dialog_about.xml +++ b/app/src/main/res/layout/dialog_about.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin"> <TextView - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/app_description" android:id="@+id/text_app_description" /> diff --git a/app/src/main/res/menu/display_parameters.xml b/app/src/main/res/menu/display_parameters.xml index 6235c15..5835e65 100644 --- a/app/src/main/res/menu/display_parameters.xml +++ b/app/src/main/res/menu/display_parameters.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 97590ce..9136570 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -44,10 +44,5 @@ limitations under the License. android:orderInCategory="102" android:title="@string/action_about" app:showAsAction="never" /> - <item - android:id="@+id/action_exit" - android:orderInCategory="103" - android:title="@string/action_exit" - app:showAsAction="never" /> </menu> diff --git a/app/src/main/res/menu/settings.xml b/app/src/main/res/menu/settings.xml index 507a733..b8dee4f 100644 --- a/app/src/main/res/menu/settings.xml +++ b/app/src/main/res/menu/settings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/values-v11/styles.xml b/app/src/main/res/values-v11/styles.xml index 62443d6..0eaf545 100644 --- a/app/src/main/res/values-v11/styles.xml +++ b/app/src/main/res/values-v11/styles.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/values-v14/styles.xml b/app/src/main/res/values-v14/styles.xml index 9134b22..35073a0 100644 --- a/app/src/main/res/values-v14/styles.xml +++ b/app/src/main/res/values-v14/styles.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml new file mode 100644 index 0000000..feff694 --- /dev/null +++ b/app/src/main/res/values-v23/styles.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2016 Arno Onken + +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. +--> +<resources> + + <!-- + Base application theme for API 23+. This theme completely replaces + AppBaseTheme from res/values/styles.xml on API 23+ devices. + --> + <style name="AppBaseTheme" parent="Theme.AppCompat"> + <!-- API 23 theme customizations can go here. --> + <item name="android:colorBackgroundFloating">@color/window_background</item> + </style> + +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml index 571b4eb..a32c092 100644 --- a/app/src/main/res/values-w820dp/dimens.xml +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..302bcd0 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2016 Arno Onken + +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. +--> +<resources> + <color name="accent">#ffff84</color> + <color name="primary">#311d00</color> + <color name="primary_dark">#000000</color> + <color name="foreground">#afa680</color> + <color name="background">#100d07</color> + <color name="text_color_primary">#fffded</color> + <color name="text_color">#fff9d4</color> + <color name="text_color_hint">#968c6a</color> + <color name="text_color_secondary">#cec499</color> + <color name="window_background">#181209</color> + <color name="separator">#fffded</color> +</resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a98ae5e..c1a0fed 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ef2dac..e655eb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,12 +20,11 @@ limitations under the License. <string name="input">Input</string> <string name="action_settings">Settings</string> <string name="prediction">Prediction</string> - <string name="edit_message">Enter a sequence of integers that were generated by a pseudo random number generator. Enter one integer per line. Refresh to get a prediction.</string> + <string name="edit_hint">Enter a sequence of numbers that were generated by a pseudo random number generator. Enter one number per line. Refresh to get a prediction.</string> <string name="action_refresh">Refresh</string> <string name="action_discard">Discard</string> <string name="action_parameters">Parameters</string> <string name="action_about">About</string> - <string name="action_exit">Exit</string> <string name="input_direct_name">Text field</string> <string name="input_file_name">File</string> <string name="input_socket_name">Socket</string> @@ -34,8 +33,10 @@ limitations under the License. <string name="socket_error_message">Could not establish socket connection</string> <string name="number_error_message">Invalid input number</string> <string name="title_activity_display_parameters">Generator parameters</string> + <string name="title_dialog_about">About</string> + <string name="about_positive">OK</string> <string name="version_name">Version</string> - <string name="copyright">Copyright © 2015 Arno Onken\nReleased under the Apache License, Version 2.0.\n</string> + <string name="copyright">Copyright © 2015, 2016 Arno Onken\nReleased under the Apache License, Version 2.0.\n</string> <string name="app_description">Predicts pseudo random numbers based on a sequence of observed numbers.\n</string> <string name="website">Website: http://github.com/asnelt/derandom/</string> <string name="title_activity_settings">Settings</string> @@ -52,7 +53,7 @@ limitations under the License. <string name="pref_history_length">History length</string> <string name="pref_history_length_summary_singular">number is kept in the input history</string> <string name="pref_history_length_summary_plural">numbers are kept in the input history</string> - <string name="pref_history_length_default_value">1024</string> + <string name="pref_history_length_default_value">2048</string> <string name="pref_parameter_base">Parameter display base</string> <string-array name="pref_parameter_base_entries"> <item>Octal</item> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index bcc5305..faae7d6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,6 +31,32 @@ limitations under the License. <!-- Application theme. --> <style name="AppTheme" parent="AppBaseTheme"> <!-- All customizations that are NOT specific to a particular API-level can go here. --> + <item name="colorAccent">@color/accent</item> + <item name="colorPrimary">@color/primary</item> + <item name="colorPrimaryDark">@color/primary_dark</item> + <item name="android:colorForeground">@color/foreground</item> + <item name="android:colorBackground">@color/background</item> + <item name="android:textColorPrimary">@color/text_color_primary</item> + <item name="android:textColor">@color/text_color</item> + <item name="android:textColorHint">@color/text_color_hint</item> + <item name="android:textColorSecondary">@color/text_color_secondary</item> + <item name="android:windowBackground">@color/window_background</item> + <item name="dialogTheme">@style/DialogTheme</item> + <item name="alertDialogTheme">@style/DialogTheme</item> + </style> + + <!-- Dialog theme. --> + <style name="DialogTheme" parent="Theme.AppCompat.Dialog"> + <item name="colorAccent">@color/accent</item> + <item name="colorPrimary">@color/primary</item> + <item name="colorPrimaryDark">@color/primary_dark</item> + <item name="android:colorForeground">@color/foreground</item> + <item name="android:colorBackground">@color/background</item> + <item name="android:textColorPrimary">@color/text_color_primary</item> + <item name="android:textColor">@color/text_color</item> + <item name="android:textColorHint">@color/text_color_hint</item> + <item name="android:textColorSecondary">@color/text_color_secondary</item> + <item name="android:windowBackground">@color/window_background</item> </style> </resources> diff --git a/app/src/main/res/xml-v14/preferences.xml b/app/src/main/res/xml-v14/preferences.xml new file mode 100644 index 0000000..0fd1513 --- /dev/null +++ b/app/src/main/res/xml-v14/preferences.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (C) 2016 Arno Onken + +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. +--> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <PreferenceCategory + android:title="@string/pref_prediction_title" + android:key="pref_key_prediction_settings"> + <SwitchPreference + android:key="pref_auto_detect" + android:title="@string/pref_auto_detect" + android:summary="@string/pref_auto_detect_summary" + android:defaultValue="true" /> + <EditTextPreference + android:key="pref_prediction_length" + android:title="@string/pref_prediction_length" + android:dialogTitle="@string/pref_prediction_length" + android:inputType="number" + android:defaultValue="@string/pref_prediction_length_default_value" /> + </PreferenceCategory> + <PreferenceCategory + android:title="@string/pref_io_title" + android:key="pref_key_io_settings"> + <SwitchPreference + android:key="pref_colored_past" + android:title="@string/pref_colored_past" + android:summary="@string/pref_colored_past_summary" + android:defaultValue="true" /> + <EditTextPreference + android:key="pref_history_length" + android:title="@string/pref_history_length" + android:dialogTitle="@string/pref_history_length" + android:inputType="number" + android:defaultValue="@string/pref_history_length_default_value" /> + <ListPreference + android:key="pref_parameter_base" + android:title="@string/pref_parameter_base" + android:dialogTitle="@string/pref_parameter_base" + android:entries="@array/pref_parameter_base_entries" + android:entryValues="@array/pref_parameter_base_values" + android:defaultValue="@string/pref_parameter_base_default" /> + <EditTextPreference + android:key="pref_socket_port" + android:title="@string/pref_socket_port" + android:dialogTitle="@string/pref_socket_port" + android:inputType="number" + android:defaultValue="@string/pref_socket_port_default_value" /> + </PreferenceCategory> +</PreferenceScreen> diff --git a/app/src/main/res/xml/backup.xml b/app/src/main/res/xml/backup.xml index 5f94330..0edbe03 100644 --- a/app/src/main/res/xml/backup.xml +++ b/app/src/main/res/xml/backup.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index dbb6bc9..d770112 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -Copyright (C) 2015 Arno Onken +Copyright (C) 2015, 2016 Arno Onken Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle b/build.gradle index 3b1fe98..0dea270 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:2.2.3' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index daea343..ebaa784 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,19 +1,6 @@ -# Copyright (C) 2015 Arno Onken -# -# 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. -#Wed Apr 10 15:27:10 PDT 2013 +#Thu Aug 18 10:28:08 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..aec9973 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,90 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle index 6631603..eaca12d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Arno Onken + * Copyright (C) 2015, 2016 Arno Onken * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. -- GitLab