diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..15a15b2
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Spender/.gitignore b/Spender/.gitignore
new file mode 100644
index 0000000..2b75303
--- /dev/null
+++ b/Spender/.gitignore
@@ -0,0 +1,13 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/Spender/.idea/encodings.xml b/Spender/.idea/encodings.xml
new file mode 100644
index 0000000..15a15b2
--- /dev/null
+++ b/Spender/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Spender/.idea/gradle.xml b/Spender/.idea/gradle.xml
new file mode 100644
index 0000000..f43d428
--- /dev/null
+++ b/Spender/.idea/gradle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/.idea/misc.xml b/Spender/.idea/misc.xml
new file mode 100644
index 0000000..bd8f489
--- /dev/null
+++ b/Spender/.idea/misc.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/.idea/runConfigurations.xml b/Spender/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/Spender/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/.gitignore b/Spender/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/Spender/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/Spender/app/build.gradle b/Spender/app/build.gradle
new file mode 100644
index 0000000..61b87be
--- /dev/null
+++ b/Spender/app/build.gradle
@@ -0,0 +1,93 @@
+apply plugin: 'com.android.application'
+apply plugin: 'de.mannodermaus.android-junit5'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "com.project.spender"
+ minSdkVersion 21
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+ }
+
+ android.applicationVariants.all { variant ->
+ variant.outputs.all {
+ def appName = "Spender"
+ outputFileName = appName+"-${variant.versionName}.apk"
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.annotation:annotation:1.0.2'
+
+ // junit 5
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.2"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.2"
+ testImplementation "org.junit.jupiter:junit-jupiter-params:5.3.2"
+
+ // junit 4
+ testImplementation "junit:junit:4.12"
+ testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.3.2"
+
+ // rule fo tests
+ androidTestImplementation "androidx.arch.core:core-testing:2.0.1"
+ testImplementation "androidx.arch.core:core-testing:2.0.1"
+
+ // androidX for testing
+ androidTestImplementation 'androidx.test:core:1.1.0'
+ androidTestImplementation 'androidx.test:runner:1.1.1'
+ androidTestImplementation 'androidx.test:rules:1.1.1'
+
+ // Hamcrest library
+ androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
+
+ // LiveData
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
+ annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0'
+ // LiveData Testing
+ androidTestImplementation 'com.jraska.livedata:testing:1.1.0'
+
+// implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.25.2'
+ implementation 'com.google.android.gms:play-services-vision:17.0.2'
+
+ implementation 'me.dm7.barcodescanner:zxing:1.9'
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
+ implementation 'com.google.android.material:material:1.0.0'
+
+ // retrofit2 + gson
+ implementation 'com.squareup.retrofit2:retrofit:2.5.0'
+ implementation 'com.google.code.gson:gson:2.8.5'
+ implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
+
+ // Dagger dependencies
+ implementation 'com.google.dagger:dagger:2.13'
+ annotationProcessor 'com.google.dagger:dagger-compiler:2.13'
+
+ //Colors
+ implementation 'com.github.matthewyork:ColoursLibrary:1.0.+@aar'
+
+ // Room db
+ def room_version = "2.1.0-beta01"
+ implementation "androidx.room:room-runtime:$room_version"
+ annotationProcessor "androidx.room:room-compiler:$room_version"
+
+ // MPAndroidChart
+ implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
+}
diff --git a/Spender/app/itemsTable.db b/Spender/app/itemsTable.db
new file mode 100644
index 0000000..6d0beed
Binary files /dev/null and b/Spender/app/itemsTable.db differ
diff --git a/Spender/app/proguard-rules.pro b/Spender/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/Spender/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/Spender/app/src/androidTest/java/com/project/spender/ExampleInstrumentedTest.java b/Spender/app/src/androidTest/java/com/project/spender/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..75b023d
--- /dev/null
+++ b/Spender/app/src/androidTest/java/com/project/spender/ExampleInstrumentedTest.java
@@ -0,0 +1,46 @@
+package com.project.spender;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.MutableLiveData;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.jraska.livedata.TestObserver;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+
+@SmallTest
+public class ExampleInstrumentedTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = ApplicationProvider.getApplicationContext();
+ assertEquals("com.project.spender", appContext.getPackageName());
+ }
+
+ @Test
+ public void getLiveDataTest() throws InterruptedException {
+
+ MutableLiveData liveData = new MutableLiveData<>();
+
+ liveData.postValue(1);
+ TestObserver.test(liveData).awaitValue().assertValue(i -> i == 1);
+ }
+}
diff --git a/Spender/app/src/androidTest/java/com/project/spender/data/CheckDaoTest.java b/Spender/app/src/androidTest/java/com/project/spender/data/CheckDaoTest.java
new file mode 100644
index 0000000..6bacb79
--- /dev/null
+++ b/Spender/app/src/androidTest/java/com/project/spender/data/CheckDaoTest.java
@@ -0,0 +1,343 @@
+package com.project.spender.data;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.LiveData;
+import androidx.room.Room;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.jraska.livedata.TestObserver;
+import com.project.spender.charts.PieChartController;
+import com.project.spender.data.entities.Check;
+import com.project.spender.data.entities.CheckWithProducts;
+import com.project.spender.data.entities.Product;
+import com.project.spender.data.entities.ProductTagJoin;
+import com.project.spender.data.entities.Tag;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertEquals;
+
+public class CheckDaoTest {
+ private CheckDao checkDao;
+ private AppDatabase db;
+
+ private List cList;
+ private List pList;
+ private List cwpList;
+ private List tList;
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Before
+ public void createDb() {
+ Context context = ApplicationProvider.getApplicationContext();
+ db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
+ checkDao = db.getCheckDao();
+ }
+
+ @Before
+ public void createChecksAndProducts() {
+ cList = new ArrayList<>(3);
+ cList.add(new Check("One", 25250, "lol", "2007-05-18T22:05:00"));
+ cList.add(new Check("Two", 26250, "kek", "2006-05-18T22:05:00"));
+ cList.add(new Check("Three", 410000, "cheburek", "2005-05-18T22:05:00"));
+ cList.add(new Check("MMM", 1000000000, "MMM", "1994-05-18T22:05:00"));
+
+ pList = new ArrayList<>(3);
+ pList.add(new Product("dumplings", 20000, 20000, 1, 0));
+ pList.add(new Product("apples", 5250, 7500, 0.7, 0));
+ pList.add(new Product("bananas", 21000, 4200, 5, 0));
+
+ cwpList = new ArrayList<>(3);
+ cwpList.add(new CheckWithProducts(cList.get(0),
+ Arrays.asList(pList.get(0), pList.get(1))));
+
+ cwpList.add(new CheckWithProducts(cList.get(1),
+ Arrays.asList(pList.get(1), pList.get(2))));
+
+ cwpList.add(new CheckWithProducts(cList.get(2),
+ Arrays.asList(pList.get(2), pList.get(0))));
+
+ cwpList.add(new CheckWithProducts(cList.get(3), new ArrayList<>()));
+
+ tList = new ArrayList<>(3);
+ tList.add(new Tag("tag1"));
+ tList.add(new Tag("tag2"));
+ tList.add(new Tag("tag3"));
+ }
+
+ @After
+ public void closeDb() {
+ db.close();
+ }
+
+ @Test
+ public void writeChecksAndReadInList() throws InterruptedException {
+
+ LiveData> liveChecks = checkDao.getAllChecks();
+
+ TestObserver.test(liveChecks)
+ .awaitValue()
+ .assertValue(List::isEmpty);
+
+ checkDao.insertCheck(cList.get(0));
+ checkDao.insertCheck(cList.get(1));
+
+ TestObserver.test(liveChecks).awaitValue();
+ List returnedChecks = liveChecks.getValue();
+
+ assertThat(returnedChecks, hasSize(2));
+ assertThat(returnedChecks, containsInAnyOrder(cList.get(0), cList.get(1)));
+ }
+
+ @Test
+ public void addSameCheck() throws InterruptedException {
+ checkDao.insertCheck(cList.get(0));
+ checkDao.insertCheck(cList.get(0));
+
+ LiveData> liveChecks = checkDao.getAllChecks();
+ TestObserver.test(liveChecks).awaitValue();
+ List returnedChecks = liveChecks.getValue();
+
+ assertThat(returnedChecks, hasSize(2));
+ assertThat(returnedChecks.get(0).getName(), equalTo("One"));
+ assertThat(returnedChecks.get(1).getName(), equalTo("One"));
+ }
+
+ @Test
+ public void writeProductsAndReadInList() throws InterruptedException {
+
+ LiveData> liveProducts = checkDao.getAllProducts();
+
+ TestObserver.test(liveProducts)
+ .awaitValue()
+ .assertValue(List::isEmpty);
+
+ long id = checkDao.insertCheck(cList.get(0));
+ for (Product e : pList) {
+ e.setCheckId(id);
+ }
+ checkDao.insertProduct(pList.get(0));
+ checkDao.insertProduct(pList.get(1));
+
+ TestObserver.test(liveProducts).awaitValue();
+ List returnedProducts = liveProducts.getValue();
+
+ assertThat(returnedProducts, hasSize(2));
+ assertThat(returnedProducts, containsInAnyOrder(pList.get(0), pList.get(1)));
+ }
+
+ @Test
+ public void writeCheckWithProductsAndReadInList() throws InterruptedException {
+
+ LiveData> liveCwp = checkDao.getAll();
+
+ TestObserver.test(liveCwp)
+ .awaitValue()
+ .assertValue(List::isEmpty);
+
+ for (CheckWithProducts e : cwpList) {
+ checkDao.insertCheckWithProducts(e);
+ e.getProducts().forEach((p) -> p.setId(0));
+ }
+
+ TestObserver.test(liveCwp).awaitValue();
+ List returnedCwp = liveCwp.getValue();
+
+ assertThat(returnedCwp, hasSize(cwpList.size()));
+ for (CheckWithProducts e : cwpList) {
+ assertThat(returnedCwp, hasItem(e));
+ }
+ }
+
+ @Test
+ public void insertSameIdWithIgnore() {
+ Tag tag = new Tag(1, "abc");
+ Tag tag2 = new Tag(2, "abc");
+ assertEquals(1, checkDao.insertTag(tag));
+ assertEquals(-1, checkDao.insertTag(tag));
+ assertEquals(-1, checkDao.insertTag(tag2));
+ }
+
+ @Test
+ public void getIdByNameTest() {
+ Tag tag = new Tag(1, "abc");
+
+ assertEquals(0, checkDao.getTagId("abc"));
+ checkDao.insertTag(tag);
+ assertEquals(1, checkDao.getTagId("abc"));
+ }
+
+ @Test
+ public void insertTagTest() throws InterruptedException {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+ Product product1 = cwpList.get(0).getProducts().get(0);
+ checkDao.insertTagForProduct(tList.get(0), product1.getId());
+ LiveData> liveTags = checkDao.getTagsByProductId(product1.getId());
+ TestObserver.test(liveTags).awaitValue();
+ assertThat(liveTags.getValue(), contains(tList.get(0)));
+ }
+
+ @Test
+ public void getTagsByProductIdTest() {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+
+ Check check = cwpList.get(0).getCheck();
+ Product product1 = cwpList.get(0).getProducts().get(0);
+ Product product2 = cwpList.get(0).getProducts().get(1);
+
+ checkDao.insertTagsForProduct(tList, product1.getId());
+ checkDao.insertTagsForProduct(tList, product2.getId());
+
+ assertThat(tList, containsInAnyOrder(Objects.requireNonNull(
+ TestObserver.test(checkDao.getTagsByProductId(product1.getId())).value().toArray())));
+
+ assertThat(tList, containsInAnyOrder(Objects.requireNonNull(TestObserver.test(
+ checkDao.getTagsByProductId(product2.getId())).value().toArray())));
+
+ assertThat(tList, containsInAnyOrder(Objects.requireNonNull(
+ TestObserver.test(checkDao.getTagsByCheckId(check.getId())).value().toArray())));
+ }
+
+ @Test
+ public void getLastIdTest() {
+ long id = checkDao.insertCheck(cList.get(0));
+ assertEquals(id, checkDao.getLastId());
+ id = checkDao.insertCheck(cList.get(1));
+ assertEquals(id, checkDao.getLastId());
+ }
+
+ @Test
+ public void deleteCheckTest() throws InterruptedException {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+ int productNumber = cwpList.get(0).getProducts().size();
+ LiveData> liveProducts = checkDao.getAllProducts();
+ assertEquals(productNumber, TestObserver.test(liveProducts).awaitValue().value().size());
+ checkDao.deleteCheckById(cwpList.get(0).getCheck().getId());
+ assertEquals(0, TestObserver.test(liveProducts).awaitValue().value().size());
+ }
+
+ @Test
+ public void deleteProductTest() throws InterruptedException {
+ long checkId = checkDao.insertCheck(cList.get(0));
+ pList.get(0).setCheckId(checkId);
+ long id = checkDao.insertProduct(pList.get(0));
+
+ LiveData> liveProducts = checkDao.getAllProducts();
+ assertEquals(1,TestObserver.test(liveProducts).awaitValue().value().size());
+ checkDao.deleteProductById(id);
+ assertEquals(0, TestObserver.test(liveProducts).awaitValue().value().size());
+ }
+
+ @Test
+ public void deleteTagTest() throws InterruptedException {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+ long productId = cwpList.get(0).getProducts().get(0).getId();
+ long tag0Id = checkDao.insertTagForProduct(tList.get(0), productId);
+ long tag1Id = checkDao.insertTagForProduct(tList.get(1), productId);
+ long tag2Id = checkDao.insertTagForProduct(tList.get(2), productId);
+
+ LiveData> liveTags = checkDao.getTagsByProductId(productId);
+
+ assertEquals(3, TestObserver.test(liveTags).awaitValue().value().size());
+ checkDao.deleteTagById(tag0Id);
+ assertEquals(2, TestObserver.test(liveTags).awaitValue().value().size());
+ checkDao.deleteTagByName(tList.get(1).getName());
+ assertEquals(1, TestObserver.test(liveTags).awaitValue().value().size());
+ checkDao.deleteProductById(productId);
+ assertThat(checkDao.getTagId(tList.get(2).getName()), greaterThan(0L));
+ checkDao.deleteAllUnusedTags();
+ assertEquals(0, TestObserver.test(liveTags).awaitValue().value().size());
+ }
+
+ @Test
+ public void deleteTagProductReflationTest() throws InterruptedException {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+ long productId = cwpList.get(0).getProducts().get(0).getId();
+ long tagId = checkDao.insertTagForProduct(tList.get(0), productId);
+
+ LiveData> liveTags = checkDao.getTagsByProductId(productId);
+
+ assertEquals(1, TestObserver.test(liveTags).awaitValue().value().size());
+ checkDao.deleteTagProductRelation(new ProductTagJoin(productId, tagId));
+ assertEquals(0, TestObserver.test(liveTags).awaitValue().value().size());
+ }
+
+ @Test
+ public void getTagsWithSumTest() throws InterruptedException {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+ Product p0 = cwpList.get(0).getProducts().get(0);
+ Product p1 = cwpList.get(0).getProducts().get(1);
+
+ long p0Id = p0.getId();
+ long p1Id = p1.getId();
+
+ checkDao.insertTagForProduct(tList.get(0), p0Id);
+ checkDao.insertTagForProduct(tList.get(1), p0Id);
+ checkDao.insertTagForProduct(tList.get(1), p1Id);
+
+ long sum0= p0.getSum();
+ long sum1= p0.getSum() + p1.getSum();
+
+ TestObserver.test(checkDao.getTagsWithSum())
+ .awaitValue()
+ .assertValue(tags -> Arrays.equals(tags.stream().map(tag -> tag.sum).sorted().toArray(), new Long[]{sum0, sum1}));
+
+ }
+
+ @Test
+ public void getCheckByDate() {
+ checkDao.insertCheck(cList.get(0));
+ checkDao.insertCheck(cList.get(3));
+ String date0 = "2007-05-18T22:05:00";
+ String date3 = "1994-05-18 22:05:00";
+ assertEquals(2, checkDao.getChecksByDate(date3, date0).size());
+ String now = "now";
+ assertEquals(2, checkDao.getChecksByDate(date3, now).size());
+ String mid = "2005-01-01";
+ assertEquals(1, checkDao.getChecksByDate(date3, mid).size());
+ }
+
+ @Test
+ public void getProductByRegExTest() throws InterruptedException {
+ checkDao.insertCheckWithProducts(cwpList.get(0));
+ long checkId = cwpList.get(0).getCheck().getId();
+ assertEquals(2,TestObserver
+ .test(checkDao.getProductByRegEx("%s%", checkId)).awaitValue().value().size());
+
+ assertEquals(1, TestObserver
+ .test(checkDao.getProductByRegEx("%d%", checkId)).awaitValue().value().size());
+ }
+
+ @Test
+ public void getTagsWithSumAndDateTest() throws InterruptedException {
+ for (int t = 0; t < 2; t++) {
+ CheckWithProducts cwp = cwpList.get(t);
+ checkDao.insertCheckWithProducts(cwp);
+ long product0Id = cwp.getProducts().get(0).getId();
+ long product1Id = cwp.getProducts().get(1).getId();
+ checkDao.insertTagForProduct(tList.get(0), product0Id);
+ checkDao.insertTagForProduct(tList.get(1), product1Id);
+ pList.forEach(product -> product.setId(0));
+ }
+ TestObserver.test(checkDao.getTagsWithSumAndDate())
+ .awaitValue()
+ .assertValue(value ->value.size() == 4);
+
+ }
+
+}
\ No newline at end of file
diff --git a/Spender/app/src/androidTest/java/com/project/spender/fns/api/NetworkManagerTestInstrumental.java b/Spender/app/src/androidTest/java/com/project/spender/fns/api/NetworkManagerTestInstrumental.java
new file mode 100644
index 0000000..42d8245
--- /dev/null
+++ b/Spender/app/src/androidTest/java/com/project/spender/fns/api/NetworkManagerTestInstrumental.java
@@ -0,0 +1,89 @@
+package com.project.spender.fns.api;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.LiveData;
+
+import com.jraska.livedata.TestObserver;
+import com.project.spender.data.ScanResult;
+import com.project.spender.fns.api.data.CheckJsonWithStatus;
+import com.project.spender.fns.api.data.NewUser;
+import com.project.spender.fns.api.data.Status;
+import com.project.spender.fns.api.data.StatusWithResponse;
+import com.project.spender.fns.api.exception.NetworkException;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+
+import static org.junit.Assert.*;
+
+public class NetworkManagerTestInstrumental {
+
+ private ScanResult scanResult1;
+ private NetworkManager networkManager = new NetworkManager();
+ // все сломается если я поменяю пароль
+ private String defaultLogin = "+79112813247";
+ private String defaultPassword = "583066";
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Before
+ public void init() {
+
+ String fn1 = "9286000100242530";
+ String fd1 = "27641";
+ String fiscalSign1 = "124643923";
+ String date1 = "20190402T1357";
+ String sum1 = "21700";
+
+ scanResult1 = new ScanResult(fn1, fd1, fiscalSign1, date1, sum1);
+ }
+
+ @Test
+ public void getCheckAsyncSimpleTest() throws InterruptedException {
+ LiveData liveData = networkManager.getCheckAsync(defaultLogin, defaultPassword, scanResult1);
+
+
+ TestObserver.test(liveData)
+ .awaitValue().assertValue(value -> value.getStatus() == Status.SENDING)
+ .awaitNextValue().assertValue(value -> value.getStatus() == Status.EXIST)
+ .awaitNextValue().assertValue(value -> value.getStatus() == Status.SUCCESS);
+
+ assertEquals("ЧИК.МАКНАГГ. 9 БКОМБО", liveData.getValue().getCheckJson().getData().items.get(0).name);
+ }
+
+ @Test
+ public void registrationAsyncSimpleTest() throws InterruptedException, NetworkException {
+ LiveData liveData = networkManager
+ .registrationAsync(new NewUser("mishockk", "qwerty@gmail.ru", "+79112813247"));
+ TestObserver.test(liveData)
+ .awaitValue().assertValue(value -> value.getStatus() == Status.SENDING)
+ .awaitNextValue().assertValue(value -> value.getStatus() == Status.WRONG_RESPONSE_ERROR);
+
+ assertEquals(NetworkManager.USER_ALREADY_EXISTS, liveData.getValue().getException().getCode());
+ }
+
+ @Test
+ public void restorePasswordAsyncSimpleTes() throws InterruptedException {
+ LiveData liveData = networkManager
+ .restorePasswordAsync("+777");
+ TestObserver.test(liveData)
+ .awaitValue().assertValue(value -> value.getStatus() == Status.SENDING)
+ .awaitNextValue().assertValue(value -> value.getStatus() == Status.WRONG_RESPONSE_ERROR);
+
+ assertEquals(NetworkManager.UNCORRECTED_PHONE, liveData.getValue().getException().getCode());
+ }
+
+ @Test
+ public void checkUserAsyncSimpleTes() throws InterruptedException {
+ LiveData liveData = networkManager
+ .checkUserAsync(defaultLogin, defaultPassword);
+ TestObserver.test(liveData)
+ .awaitValue().assertValue(value -> value.getStatus() == Status.SENDING)
+ .awaitNextValue().assertValue(value -> value.getStatus() == Status.SUCCESS);
+
+ }
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/AndroidManifest.xml b/Spender/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b6830d2
--- /dev/null
+++ b/Spender/app/src/main/AndroidManifest.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/activities/CheckShowActivity.java b/Spender/app/src/main/java/com/project/spender/activities/CheckShowActivity.java
new file mode 100644
index 0000000..1b56145
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/CheckShowActivity.java
@@ -0,0 +1,115 @@
+package com.project.spender.activities;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.project.spender.R;
+import com.project.spender.controllers.CheckShowHelper;
+
+import static com.project.spender.controllers.TagChoiceHelper.TAG_ID_LIST;
+
+/**
+ * Activity for showing products in the check
+ */
+public class CheckShowActivity extends AppCompatActivity implements LifecycleOwner {
+
+ private CheckShowHelper controller;
+ private LifecycleRegistry lifecycleRegistry;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ }
+
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ private static final int ADDING_CODE = 20;
+ private static final int REMOVING_CODE = 40;
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (data == null) {
+ return;
+ }
+
+ long[] tags = data.getLongArrayExtra(TAG_ID_LIST);
+ if (tags == null) {
+ return;
+ }
+
+ if (resultCode == Activity.RESULT_OK) {
+ if (requestCode == ADDING_CODE) {
+ controller.addTags(tags);
+ } else if (requestCode == REMOVING_CODE) {
+ controller.removeTags(tags);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.products_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.remove:
+ controller.removeProducts();
+ return true;
+
+ case R.id.add_tag:
+ Intent intentTagChoice = new Intent(this, TagChoiceActivity.class);
+ startActivityForResult(intentTagChoice, ADDING_CODE);
+ return true;
+ case R.id.remove_tag:
+ intentTagChoice = new Intent(this, TagChoiceActivity.class);
+ startActivityForResult(intentTagChoice, REMOVING_CODE);
+ return true;
+
+ default:
+
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_check_show);
+
+
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ ListView listView = findViewById(R.id.productsList);
+ EditText search = findViewById(R.id.search_in_check);
+
+ controller = new CheckShowHelper(this, listView, getIntent());
+ controller.setInput(search);
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/HelpActivity.java b/Spender/app/src/main/java/com/project/spender/activities/HelpActivity.java
new file mode 100644
index 0000000..7a0aaef
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/HelpActivity.java
@@ -0,0 +1,23 @@
+package com.project.spender.activities;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.widget.Button;
+
+import com.project.spender.R;
+import com.project.spender.roller.ChecksRoller;
+
+/**
+ * Activity for showing helpful information
+ */
+public class HelpActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_help);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/HistoryActivity.java b/Spender/app/src/main/java/com/project/spender/activities/HistoryActivity.java
new file mode 100644
index 0000000..ae688aa
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/HistoryActivity.java
@@ -0,0 +1,35 @@
+package com.project.spender.activities;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.project.spender.R;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import javax.inject.Inject;
+
+/**
+ * Activity for showing list of check requests and its statuses in current session
+ */
+public class HistoryActivity extends AppCompatActivity {
+
+ @Inject ChecksRoller checksRoller;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ App.getComponent().inject(this);
+ setContentView(R.layout.activity_history);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+
+ TextView textView = findViewById(R.id.info_about_history);
+ ListView listView = findViewById(R.id.history_list);
+ checksRoller.getHistoryListHolder().setListView(this, listView, textView);
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/ListActivity.java b/Spender/app/src/main/java/com/project/spender/activities/ListActivity.java
new file mode 100644
index 0000000..1fbebd3
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/ListActivity.java
@@ -0,0 +1,222 @@
+package com.project.spender.activities;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.project.spender.controllers.CheckListHolder;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.R;
+import com.project.spender.data.ScanResult;
+
+import static com.project.spender.charts.ChartsStateHolder.hideKeyboard;
+import static com.project.spender.controllers.TagChoiceHelper.TAG_ID_LIST;
+
+
+/**
+ * Activity for showing list of checks or products
+ */
+public class ListActivity extends AppCompatActivity implements LifecycleOwner {
+
+ private EditText request;
+
+ private LifecycleRegistry lifecycleRegistry;
+
+ private CheckListHolder holder;
+
+ private EditText beginDate;
+ private EditText endDate;
+
+ private static final int CHOOSE_TAG_FOR_ADD = 123;
+ private static final int CHOOSE_TAG_FOR_REMOVE = 124;
+ private static final int CHOOSE_TAG_FOR_SHOW = 125;
+ private static final int SCAN_CODE = 42;
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == SCAN_CODE) {
+ Toast.makeText(this, ScanResult.explain(resultCode), Toast.LENGTH_LONG).show();
+ }
+
+
+ if (data == null) {
+ return;
+ }
+
+ if (resultCode == Activity.RESULT_OK) {
+
+ long[] tagIds = data.getLongArrayExtra(TAG_ID_LIST);
+
+ if (requestCode == CHOOSE_TAG_FOR_SHOW) {
+ holder.setTags(tagIds);
+ } else if (requestCode == CHOOSE_TAG_FOR_ADD) {
+ holder.addTagsForItem(tagIds);
+ } else if (requestCode == CHOOSE_TAG_FOR_REMOVE) {
+ holder.removeTagsForItem(tagIds);
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ }
+
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ getMenuInflater().inflate(R.menu.list_view_context_menu, menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ switch(item.getItemId()) {
+ case R.id.add_tag_for_check:
+ Intent intent = new Intent(this, TagChoiceActivity.class);
+ holder.chooseItem(info.position);
+ startActivityForResult(intent, CHOOSE_TAG_FOR_ADD);
+ break;
+
+ case R.id.remove_tag_for_check:
+ intent = new Intent(this, TagChoiceActivity.class);
+ holder.chooseItem(info.position);
+ startActivityForResult(intent, CHOOSE_TAG_FOR_REMOVE);
+ break;
+
+ case R.id.remove_check:
+ holder.removeItem(info.position);
+ break;
+
+ default:
+ return super.onContextItemSelected(item);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.list_view_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.app_bar_switch:
+ holder.changeMode();
+ item.setChecked(holder.getMode());
+ if (holder.getMode()) {
+ beginDate.setVisibility(View.GONE);
+ endDate.setVisibility(View.GONE);
+ } else {
+ beginDate.setVisibility(View.VISIBLE);
+ endDate.setVisibility(View.VISIBLE);
+ }
+ break;
+ case R.id.set_tags_for_list:
+ Intent intent = new Intent(this, TagChoiceActivity.class);
+ startActivityForResult(intent, CHOOSE_TAG_FOR_SHOW);
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ setContentView(R.layout.activity_list);
+
+ TextView info = findViewById(R.id.info_about_list);
+
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+
+
+ ImageButton scan = findViewById(R.id.scan);
+ ImageButton statistics = findViewById(R.id.statistics);
+ ImageButton list = findViewById(R.id.list);
+ list.setImageResource(R.drawable.history_chosen);
+
+ statistics.setOnClickListener(v -> {
+ finish();
+ });
+
+ scan.setOnClickListener(v -> {
+ final Intent intent = new Intent(ListActivity.this, ScanActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ startActivityForResult(intent, SCAN_CODE);
+ });
+
+ ListView listView = findViewById(R.id.productsList);
+ registerForContextMenu(listView);
+
+ holder = new CheckListHolder(listView, this, info);
+
+ request = findViewById(R.id.request);
+ request.setOnEditorActionListener((v, actionId, event) -> {
+
+ holder.setSubstring(request.getText().toString());
+
+ hideKeyboard(v);
+ return true;
+ });
+
+ beginDate = findViewById(R.id.begin_date);
+ beginDate.setOnEditorActionListener((v, actionId, event) -> {
+
+ Log.i(ChecksRoller.LOG_TAG, "Got begin value " + beginDate.getText().toString());
+ try {
+ holder.setBegin(beginDate.getText().toString());
+ } catch (IllegalArgumentException e) {
+ Log.i(ChecksRoller.LOG_TAG, "Invalid format ");
+ Toast.makeText(ListActivity.this, "invalid data format", Toast.LENGTH_SHORT).show();
+ }
+ hideKeyboard(v);
+ return true;
+ });
+
+ endDate = findViewById(R.id.end_date);
+ endDate.setOnEditorActionListener((v, actionId, event) -> {
+ Log.i(ChecksRoller.LOG_TAG, "Got end value " + endDate.getText().toString());
+ try {
+ holder.setEnd(endDate.getText().toString());
+ } catch (IllegalArgumentException e) {
+ Log.i(ChecksRoller.LOG_TAG, "Invalid format ");
+ Toast.makeText(ListActivity.this, "invalid data format", Toast.LENGTH_SHORT).show();
+ }
+ hideKeyboard(v);
+ return true;
+ });
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/LoginActivity.java b/Spender/app/src/main/java/com/project/spender/activities/LoginActivity.java
new file mode 100644
index 0000000..71bd93e
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/LoginActivity.java
@@ -0,0 +1,124 @@
+package com.project.spender.activities;
+
+import android.app.Activity;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.R;
+import com.project.spender.fns.api.NetworkManager;
+import com.project.spender.fns.api.data.Status;
+import com.project.spender.fns.api.data.StatusWithResponse;
+
+import javax.inject.Inject;
+
+/**
+ * Activity for login, register and password restore actions
+ */
+public class LoginActivity extends AppCompatActivity {
+
+ @Inject ChecksRoller checksRoller;
+
+ private EditText password;
+ private EditText number;
+ private EditText name;
+ private EditText email;
+ private Button catButton;
+ private Button enter;
+ private Button register;
+ private Button restore;
+
+
+ private void setRegisterMode() {
+ catButton.setVisibility(View.GONE);
+ register.setVisibility(View.GONE);
+ password.setVisibility(View.GONE);
+ name.setVisibility(View.VISIBLE);
+ email.setVisibility(View.VISIBLE);
+ restore.setVisibility(View.GONE);
+
+ enter.setOnClickListener(v -> {
+ String newEmail = email.getText().toString();
+ String newNumber = number.getText().toString();
+ String newName = name.getText().toString();
+ LiveData result = checksRoller.registration(newName, newEmail, newNumber);
+ result.observeForever(status -> {
+ Toast.makeText(LoginActivity.this, status.getUserReadableMassage(), Toast.LENGTH_LONG).show();
+ if (status.getStatus().equals(Status.SUCCESS)) {
+ setLoginMode();
+ }
+ });
+ });
+ }
+
+
+ private void setLoginMode() {
+ if (checksRoller.getCatMode()) {
+ catButton.setVisibility(View.VISIBLE);
+ } else {
+ catButton.setVisibility(View.GONE);
+ }
+
+ register.setVisibility(View.VISIBLE);
+ password.setVisibility(View.VISIBLE);
+ restore.setVisibility(View.VISIBLE);
+ name.setVisibility(View.GONE);
+ email.setVisibility(View.GONE);
+ enter.setOnClickListener(v -> {
+ String newPassword = password.getText().toString();
+ String newNumber = number.getText().toString();
+ if (!newNumber.matches("\\+\\d{11}")) {
+ Toast.makeText(LoginActivity.this, "invalid number format", Toast.LENGTH_SHORT).show();
+ } else if (!newPassword.matches("\\d{6}")) {
+ Toast.makeText(LoginActivity.this, "invalid password format", Toast.LENGTH_SHORT).show();
+ } else {
+ checksRoller.saveAccountInfo(newNumber, newPassword);
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ });
+
+ restore.setOnClickListener(v -> {
+ String num = number.getText().toString();
+ LiveData result = checksRoller.restore(num);
+ result.observeForever(status -> Toast.makeText(LoginActivity.this,
+ status.getUserReadableMassage(), Toast.LENGTH_SHORT).show());
+
+ });
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_login);
+ App.getComponent().inject(this);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ password = findViewById(R.id.password);
+ number = findViewById(R.id.number);
+ catButton = findViewById(R.id.im_cat);
+ catButton.setOnClickListener(v -> {
+ checksRoller.saveAccountInfo(NetworkManager.DEFAULT_LOGIN, NetworkManager.DEFAULT_PASSWORD);
+ finish();
+ });
+
+ restore = findViewById(R.id.restore_password);
+ enter = findViewById(R.id.enter);
+
+ name = findViewById(R.id.new_name);
+ email = findViewById(R.id.email);
+
+ register = findViewById(R.id.register);
+ register.setOnClickListener(v -> setRegisterMode());
+
+ setLoginMode();
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/MainActivity.java b/Spender/app/src/main/java/com/project/spender/activities/MainActivity.java
new file mode 100644
index 0000000..e1b3856
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/MainActivity.java
@@ -0,0 +1,237 @@
+package com.project.spender.activities;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.project.spender.R;
+import com.project.spender.charts.ChartsStateHolder;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.data.ScanResult;
+import com.project.spender.fragments.LineChartFragment;
+import com.project.spender.fragments.PieChartFragment;
+import com.project.spender.fragments.StackedBarChartFragment;
+
+import javax.inject.Inject;
+
+import static com.project.spender.controllers.TagChoiceHelper.TAG_ID_LIST;
+
+/**
+ * Activity with diagram and main menu
+ */
+public class MainActivity extends AppCompatActivity implements LifecycleOwner {
+
+ @Inject protected ChecksRoller checksRoller;
+
+ private LifecycleRegistry lifecycleRegistry;
+ private ChartsStateHolder chartsStateHolder;
+
+ private final static int CAMERA_REQUEST = 1;
+ private final static int CHART_TAGS_CODE = 15325;
+ private final static int CHECK_REQUEST = 42;
+ private final static int LOGIN_CODE = 12400;
+
+ private FragmentManager fragmentManager;
+ private PieChartFragment pieFragment;
+ private LineChartFragment lineFragment;
+ private StackedBarChartFragment barFragment;
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == CHECK_REQUEST) {
+
+ Toast.makeText(this, ScanResult.explain(resultCode), Toast.LENGTH_LONG).show();
+
+ } else if (requestCode == CHART_TAGS_CODE) {
+
+ if (data == null) {
+ return;
+ }
+ long[] ids = data.getLongArrayExtra(TAG_ID_LIST);
+ if (ids == null) {
+ return;
+ }
+ chartsStateHolder.setIds(ids);
+ } else if (requestCode == LOGIN_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == CAMERA_REQUEST) {
+ if (grantResults.length == 0
+ || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, "Camera permission not got",
+ Toast.LENGTH_LONG).show();
+ Log.i(ChecksRoller.LOG_TAG, "didnt get camera permission");
+ } else {
+ Log.i(ChecksRoller.LOG_TAG, "got camera permission");
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.settings, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ switch (item.getItemId()) {
+ case R.id.action_cheese:
+ checksRoller.cheese();
+ return true;
+
+ case R.id.action_delete:
+ checksRoller.onRemoveAllClicked();
+ return true;
+ case R.id.help:
+ startActivity(new Intent(this, HelpActivity.class));
+ return true;
+
+ case R.id.receiving_history:
+ startActivity(new Intent(this, HistoryActivity.class));
+ return true;
+
+ case R.id.action_tag_list:
+ Intent intent = new Intent(this, TagListActivity.class);
+ startActivity(intent);
+ return true;
+
+ case R.id.putin:
+ startActivityForResult(new Intent(this, LoginActivity.class), LOGIN_CODE);
+ break;
+ case R.id.putout:
+ checksRoller.clearAccountInfo();
+ break;
+ case R.id.pie_chart_item:
+ Log.i(ChecksRoller.LOG_TAG, "pie chart");
+ fragmentTransaction.replace(R.id.fragmentHolder, pieFragment);
+ fragmentTransaction.commit();
+ chartsStateHolder.setChartFragment(pieFragment);
+ pieFragment.drawHole(false);
+ break;
+ case R.id.donut_chart_item:
+ Log.i(ChecksRoller.LOG_TAG, "donut chart");
+ fragmentTransaction.replace(R.id.fragmentHolder, pieFragment);
+ fragmentTransaction.commit();
+ chartsStateHolder.setChartFragment(pieFragment);
+ pieFragment.drawHole(true);
+ break;
+ case R.id.line_graph_item:
+ Log.i(ChecksRoller.LOG_TAG, "line graph");
+ fragmentTransaction.replace(R.id.fragmentHolder, lineFragment);
+ fragmentTransaction.commit();
+ chartsStateHolder.setChartFragment(lineFragment);
+ break;
+ case R.id.bar_graph_item:
+ Log.i(ChecksRoller.LOG_TAG, "bar graph");
+ fragmentTransaction.replace(R.id.fragmentHolder, barFragment);
+ fragmentTransaction.commit();
+ chartsStateHolder.setChartFragment(barFragment);
+ break;
+ case R.id.tags_for_chart:
+ startActivityForResult(new Intent(this, TagChoiceActivity.class), CHART_TAGS_CODE);
+ break;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ }
+
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ App.getComponent().inject(this);
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+
+
+ checksRoller.setOwner(this);
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[] {Manifest.permission.CAMERA}, CAMERA_REQUEST);
+ }
+
+ setContentView(R.layout.activity_main);
+
+ ImageButton scan = findViewById(R.id.scan);
+ ImageButton list = findViewById(R.id.list);
+ ImageButton statistics = findViewById(R.id.statistics);
+
+ statistics.setImageResource(R.drawable.piechart_chosen);
+
+ list.setOnClickListener(v -> {
+ final Intent intentShowList = new Intent(MainActivity.this, ListActivity.class);
+ startActivity(intentShowList);
+ });
+
+ scan.setOnClickListener(v -> {
+ final Intent intent = new Intent(MainActivity.this, ScanActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ startActivityForResult(intent, CHECK_REQUEST);
+ });
+
+ EditText begin = findViewById(R.id.begin_date_for_chart);
+ EditText end = findViewById(R.id.end_date_for_chart);
+
+ fragmentManager = getSupportFragmentManager();
+ pieFragment = PieChartFragment.newInstance();
+ lineFragment = LineChartFragment.newInstance();
+ barFragment = StackedBarChartFragment.newInstance();
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ fragmentTransaction.add(R.id.fragmentHolder, pieFragment);
+ fragmentTransaction.commit();
+
+ chartsStateHolder = new ChartsStateHolder();
+ chartsStateHolder.setBeginDateInput(this, begin);
+ chartsStateHolder.setEndDateInput(this, end);
+ chartsStateHolder.setChartFragment(pieFragment);
+
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/NewTagActivity.java b/Spender/app/src/main/java/com/project/spender/activities/NewTagActivity.java
new file mode 100644
index 0000000..34a555e
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/NewTagActivity.java
@@ -0,0 +1,67 @@
+package com.project.spender.activities;
+
+import com.google.android.material.textfield.TextInputEditText;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.app.Activity;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Button;
+
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.R;
+import com.project.spender.controllers.TagStateHolder;
+
+import static com.project.spender.charts.ChartsStateHolder.hideKeyboard;
+
+/**
+ * Activity for adding new tag
+ */
+public class NewTagActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_new_tag);
+
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ Button enter = findViewById(R.id.enter_new_tag_info);
+ TextInputEditText name = findViewById(R.id.new_tag_name);
+ TextInputEditText color = findViewById(R.id.new_tag_color);
+ TextInputEditText substring = findViewById(R.id.new_tag_substring);
+
+ TagStateHolder tagStateHolder = new TagStateHolder();
+
+ name.setOnEditorActionListener((textView, i, keyEvent) -> {
+ Log.i(ChecksRoller.LOG_TAG, name.getText().toString() + "");
+ tagStateHolder.setName(name.getText().toString());
+ hideKeyboard(textView);
+ return true;
+ });
+
+ color.setOnEditorActionListener((textView, i, keyEvent) -> {
+ tagStateHolder.setColor(color.getText().toString());
+ hideKeyboard(textView);
+ return true;
+ });
+
+ substring.setOnEditorActionListener((textView, i, keyEvent) -> {
+ tagStateHolder.setRegEx(substring.getText().toString());
+ hideKeyboard(textView);
+ return true;
+ });
+
+ enter.setOnClickListener(v -> {
+ tagStateHolder.setName(name.getText().toString());
+ tagStateHolder.setColor(color.getText().toString());
+ tagStateHolder.setRegEx(substring.getText().toString());
+ tagStateHolder.createTag();
+ setResult(Activity.RESULT_OK);
+ finish();
+ });
+ }
+}
+
+
diff --git a/Spender/app/src/main/java/com/project/spender/activities/ScanActivity.java b/Spender/app/src/main/java/com/project/spender/activities/ScanActivity.java
new file mode 100644
index 0000000..39e682d
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/ScanActivity.java
@@ -0,0 +1,70 @@
+package com.project.spender.activities;
+
+import android.app.Activity;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.zxing.Result;
+import com.project.spender.data.entities.Check;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.data.ScanResult;
+
+import javax.inject.Inject;
+
+import me.dm7.barcodescanner.zxing.ZXingScannerView;
+
+/**
+ * Activity for scanning qr-code
+ */
+public class ScanActivity extends AppCompatActivity implements ZXingScannerView.ResultHandler{
+
+ @Inject protected ChecksRoller checksRoller;
+ private ZXingScannerView scannerView;
+
+ @Override
+ public void onCreate(Bundle state) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ super.onCreate(state);
+ App.getComponent().inject(this);
+ scannerView = new ZXingScannerView(this);
+ setContentView(scannerView);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ scannerView.setResultHandler(this);
+ scannerView.startCamera();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ scannerView.stopCamera();
+ }
+
+ @Override
+ public void handleResult(Result result) {
+ int putResult;
+ try {
+ ScanResult scanResult = new ScanResult(result.getText());
+ putResult = checksRoller.requestCheck(scanResult);
+ } catch (Exception e) {
+ putResult = -2;
+ }
+
+ if (putResult == -1) {
+ setResult(ScanResult.NOT_ENOUGH_DATA);
+ } else if (putResult == -2) {
+ setResult(ScanResult.WRONG_CODE);
+ } else if (putResult != 0) {
+ setResult(Activity.RESULT_CANCELED);
+ } else {
+ setResult(Activity.RESULT_OK);
+ }
+ finish();
+ }
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/activities/TagChoiceActivity.java b/Spender/app/src/main/java/com/project/spender/activities/TagChoiceActivity.java
new file mode 100644
index 0000000..765eb5e
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/TagChoiceActivity.java
@@ -0,0 +1,78 @@
+package com.project.spender.activities;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Button;
+import android.widget.ListView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.project.spender.R;
+import com.project.spender.controllers.TagChoiceHelper;
+
+/**
+ * Activity for choosing list of tags for action
+ */
+public class TagChoiceActivity extends AppCompatActivity implements LifecycleOwner {
+
+ private TagChoiceHelper controller;
+ private LifecycleRegistry lifecycleRegistry;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.tag_choice_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add_tag:
+ startActivity(new Intent(this, NewTagActivity.class));
+ break;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_tag_choice);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+
+ ListView listView = findViewById(R.id.tag_list);
+ controller = new TagChoiceHelper(this, listView);
+
+ Button enter = findViewById(R.id.enter_tag_set);
+ enter.setOnClickListener(view -> {
+ setResult(Activity.RESULT_OK, controller.resultIntent());
+ finish();
+ });
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/activities/TagListActivity.java b/Spender/app/src/main/java/com/project/spender/activities/TagListActivity.java
new file mode 100644
index 0000000..6a63884
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/activities/TagListActivity.java
@@ -0,0 +1,115 @@
+package com.project.spender.activities;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.project.spender.R;
+import com.project.spender.controllers.TagListHelper;
+
+/**
+ * Activity for showing all tags
+ */
+public class TagListActivity extends AppCompatActivity implements LifecycleOwner {
+
+ TagListHelper controller;
+
+ private LifecycleRegistry lifecycleRegistry;
+
+ private final int NEW_TAG_CODE = 76;
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ if (requestCode == NEW_TAG_CODE) {
+ controller.update(this);
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ lifecycleRegistry.markState(Lifecycle.State.STARTED);
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycleRegistry;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if (v.getId() == R.id.tag_list_menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.tag_list_context_menu, menu);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.tag_list_menu, menu);
+ return true;
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.new_tag_add:
+ Intent intent = new Intent(this, NewTagActivity.class);
+ startActivityForResult(intent, NEW_TAG_CODE);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ switch(item.getItemId()) {
+ case R.id.tag_list_delete:
+ controller.remove(info.position);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_tag_list);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+ lifecycleRegistry = new LifecycleRegistry(this);
+ lifecycleRegistry.markState(Lifecycle.State.CREATED);
+
+ ListView listView = findViewById(R.id.tag_list_menu);
+ registerForContextMenu(listView);
+
+ TextView textView = findViewById(R.id.info_about_tags);
+
+ controller = new TagListHelper(this, listView, textView);
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/adapters/HistoryItemAdapter.java b/Spender/app/src/main/java/com/project/spender/adapters/HistoryItemAdapter.java
new file mode 100644
index 0000000..b405694
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/adapters/HistoryItemAdapter.java
@@ -0,0 +1,68 @@
+package com.project.spender.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.project.spender.R;
+import com.project.spender.data.CheckStatus;
+
+import java.util.List;
+
+import static android.content.Context.LAYOUT_INFLATER_SERVICE;
+
+/**
+ * Adapter for check requests status list
+ */
+public class HistoryItemAdapter extends BaseAdapter {
+
+ private LayoutInflater lInflater;
+ private List list;
+
+ public HistoryItemAdapter(Context context, List list) {
+ this.list = list;
+ lInflater = (LayoutInflater) context
+ .getSystemService(LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public int getCount() {
+ return list.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return list.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = lInflater.inflate(R.layout.history_item, parent, false);
+ }
+
+ final View view = convertView;
+
+ CheckStatus item = getCheckStatus(position);
+
+
+ ((TextView) view.findViewById(R.id.name)).setText(item.getStatus());
+ ((TextView) view.findViewById(R.id.time)).setText("Requested : " + item.getTime() );
+
+ return view;
+ }
+
+ private CheckStatus getCheckStatus(int position) {
+ return (CheckStatus) getItem(position);
+ }
+
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/adapters/ItemAdapter.java b/Spender/app/src/main/java/com/project/spender/adapters/ItemAdapter.java
new file mode 100644
index 0000000..96e2c0d
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/adapters/ItemAdapter.java
@@ -0,0 +1,125 @@
+package com.project.spender.adapters;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.R;
+import com.project.spender.controllers.TagListHelper;
+import com.project.spender.data.entities.Product;
+import com.project.spender.data.entities.Tag;
+
+import java.util.HashSet;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import static android.content.Context.LAYOUT_INFLATER_SERVICE;
+import static com.project.spender.controllers.CheckShowHelper.SELECTED_ITEM;
+import static com.project.spender.controllers.CheckShowHelper.UNSELECTED_ITEM;
+
+/**
+ * Adapter for products list
+ */
+public class ItemAdapter extends BaseAdapter {
+
+ @Inject ChecksRoller checksRoller;
+
+ private Context context;
+ private LayoutInflater lInflater;
+ private List productList;
+ private LifecycleOwner owner;
+ @Nullable private HashSet chosen;
+
+ public ItemAdapter(Context context, List products, @Nullable HashSet hashSet) {
+ chosen = hashSet;
+ this.context = context;
+ owner = (LifecycleOwner) context;
+ productList = products;
+ lInflater = (LayoutInflater) context
+ .getSystemService(LAYOUT_INFLATER_SERVICE);
+ App.getComponent().inject(this);
+ }
+
+ @Override
+ public int getCount() {
+ return productList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return productList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = lInflater.inflate(R.layout.item_view, parent, false);
+ } else if (chosen != null) {
+ if (chosen.contains(position)) {
+ convertView.setBackgroundColor(SELECTED_ITEM);
+ } else {
+ convertView.setBackgroundColor(UNSELECTED_ITEM);
+ }
+ }
+
+ final View view = convertView;
+
+ Product product = getProduct(position);
+
+ ((TextView) view.findViewById(R.id.name)).setText(product.getName());
+ ((TextView) view.findViewById(R.id.price)).setText("Price: " +
+ String.format("%.2f", product.getPrice() / 100.0));
+ ((TextView) view.findViewById(R.id.count)).setText("Quantity: " + product.getQuantity());
+
+ LinearLayout layout = view.findViewById(R.id.linear_layout);
+
+ LiveData> tags = checksRoller.getAppDatabase().getCheckDao().getTagsByProductId(product.getId());
+ final LinearLayout linearLayout = layout;
+ tags.observe(owner, tags1 -> {
+ updateTags(tags1, linearLayout);
+ for (Tag t : tags1) {
+ Log.i(ChecksRoller.LOG_TAG, product.getName() + " " + t);
+ }
+ });
+
+
+
+ return view;
+ }
+
+ private Product getProduct(int position) {
+ return (Product) getItem(position);
+ }
+
+ private void updateTags(List tags, LinearLayout layout1) {
+
+ Adapter adapter = new TagAdapter(context, tags);
+ layout1.removeAllViews();
+ for (int i = 0; i < tags.size(); i++) {
+ View child = adapter.getView(i, null, null);
+ child.setBackgroundColor(tags.get(i).getColor());
+ layout1.addView(child, TagListHelper.tagParams());
+ }
+ layout1.invalidate();
+ }
+
+}
+
diff --git a/Spender/app/src/main/java/com/project/spender/adapters/ListAdapter.java b/Spender/app/src/main/java/com/project/spender/adapters/ListAdapter.java
new file mode 100644
index 0000000..11d8592
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/adapters/ListAdapter.java
@@ -0,0 +1,103 @@
+package com.project.spender.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.R;
+import com.project.spender.controllers.TagListHelper;
+import com.project.spender.data.entities.Check;
+import com.project.spender.data.entities.CheckWithProducts;
+import com.project.spender.data.entities.Tag;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Adapter for checks list
+ */
+public class ListAdapter extends BaseAdapter {
+ private Context context;
+ private LayoutInflater layoutInflater;
+ private List checkList;
+ private LifecycleOwner owner;
+ @Inject protected ChecksRoller checksRoller;
+
+ public ListAdapter(Context context, List list) {
+ this.context = context;
+ checkList = list;
+ owner = (LifecycleOwner) context;
+ layoutInflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ App.getComponent().inject(this);
+ }
+
+ @Override
+ public int getCount() {
+ return checkList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return checkList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = layoutInflater.inflate(R.layout.check_list_view, parent, false);
+ }
+
+ final View view = convertView;
+
+ Check check = getCheck(position);
+
+
+ LiveData> tags = checksRoller.getAppDatabase()
+ .getCheckDao().getTagsByCheckId(check.getId());
+ final LinearLayout linearLayout = view.findViewById(R.id.check_tag_list);
+ tags.observe(owner, tags1 -> updateTags(tags1, linearLayout));
+
+
+ ((TextView) view.findViewById(R.id.name)).setText(check.getName());
+ ((TextView) view.findViewById(R.id.sum)).setText("Total: " +
+ String.format("%.2f", check.getTotalSum() / 100.0));
+ ((TextView) view.findViewById(R.id.data)).setText("Time: " + check.getDate().replace("T", " "));
+ ((TextView) view.findViewById(R.id.shop)).setText("Shop: " + check.getShop());
+
+ return view;
+ }
+
+ private Check getCheck(int position) {
+ return ((CheckWithProducts) getItem(position)).getCheck() ;
+ }
+
+ private void updateTags(List tags, LinearLayout layout1) {
+ Adapter adapter = new TagAdapter(context, tags);
+ layout1.removeAllViews();
+ for (int i = 0; i < tags.size(); i++) {
+ View child = adapter.getView(i, null, null);
+ child.setBackgroundColor(tags.get(i).getColor());
+
+ layout1.addView(child, TagListHelper.tagParams());
+ }
+ layout1.invalidate();
+ }
+}
+
diff --git a/Spender/app/src/main/java/com/project/spender/adapters/TagAdapter.java b/Spender/app/src/main/java/com/project/spender/adapters/TagAdapter.java
new file mode 100644
index 0000000..c05fb61
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/adapters/TagAdapter.java
@@ -0,0 +1,62 @@
+package com.project.spender.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.project.spender.R;
+import com.project.spender.data.entities.Tag;
+
+import java.util.List;
+
+/**
+ * Adapter for products list
+ */
+public class TagAdapter extends BaseAdapter {
+ private LayoutInflater lInflater;
+ private List tagsList;
+
+ TagAdapter(Context context, List tags) {
+ tagsList = tags;
+ lInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public int getCount() {
+ return tagsList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return tagsList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = lInflater.inflate(R.layout.tag_view, parent, false);
+ }
+
+ final View view = convertView;
+
+ Tag tag = getProduct(position);
+
+ View color = view.findViewById(R.id.color);
+ color.setBackgroundColor(tag.getColor());
+
+ return view;
+ }
+
+ private Tag getProduct(int position) {
+ return (Tag) getItem(position);
+ }
+
+}
+
diff --git a/Spender/app/src/main/java/com/project/spender/adapters/TagChoiceAdapter.java b/Spender/app/src/main/java/com/project/spender/adapters/TagChoiceAdapter.java
new file mode 100644
index 0000000..fb339bc
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/adapters/TagChoiceAdapter.java
@@ -0,0 +1,77 @@
+package com.project.spender.adapters;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.gms.vision.L;
+import com.project.spender.R;
+import com.project.spender.data.entities.Tag;
+
+import java.util.List;
+
+import static com.project.spender.controllers.CheckShowHelper.SELECTED_ITEM;
+import static com.project.spender.controllers.CheckShowHelper.UNSELECTED_ITEM;
+
+/**
+ * Adapter for tags available to choose list
+ */
+public class TagChoiceAdapter extends BaseAdapter {
+ private LayoutInflater layoutInflater;
+ private @NonNull List tagList;
+ private @Nullable List chosenTags;
+
+ public TagChoiceAdapter(Context context, @NonNull List list, @Nullable List chosen) {
+ chosenTags = chosen;
+ tagList = list;
+ layoutInflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public int getCount() {
+ return tagList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return tagList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = layoutInflater.inflate(R.layout.tag_choice_item_view, parent, false);
+ }
+
+ Tag tag = tagList.get(position);
+
+ if (chosenTags != null) {
+ if (chosenTags.contains(tag)) {
+ convertView.setBackgroundColor(SELECTED_ITEM);
+ } else {
+ convertView.setBackgroundColor(UNSELECTED_ITEM);
+ }
+ }
+
+ ((TextView) convertView.findViewById(R.id.tag_in_list_name)).setText(tag.getName());
+ if (tag.getSubstring() != null) {
+ ((TextView) convertView.findViewById(R.id.tag_in_list_substring)).setText("auto adding by " + tag.getSubstring());
+ }
+ convertView.findViewById(R.id.tag_in_list_color).setBackgroundColor(tag.getColor());
+
+ return convertView;
+ }
+}
+
diff --git a/Spender/app/src/main/java/com/project/spender/charts/ChartsStateHolder.java b/Spender/app/src/main/java/com/project/spender/charts/ChartsStateHolder.java
new file mode 100644
index 0000000..8b3da6a
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/charts/ChartsStateHolder.java
@@ -0,0 +1,89 @@
+package com.project.spender.charts;
+
+import android.content.Context;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.project.spender.controllers.DateHelper;
+import com.project.spender.fragments.ChartFragment;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Class to store time period and list of tags for chart and its fragment.
+ */
+public class ChartsStateHolder {
+
+ private String beginDate = DateHelper.DEFAULT_BEGIN;
+ private String endDate = DateHelper.DEFAULT_END;
+ private long[] ids;
+
+ private ChartFragment chartFragment;
+
+ public void setBeginDateInput(Context context, @NonNull EditText editText) {
+ editText.setOnEditorActionListener((textView, i, keyEvent) -> {
+ try {
+ beginDate = DateHelper.dateConvert(textView.getText().toString());
+ } catch (Exception e) {
+ beginDate = DateHelper.DEFAULT_BEGIN;
+ if (!editText.getText().toString().equals("")) {
+ Toast.makeText(context, "Invalid data format", Toast.LENGTH_SHORT).show();
+ }
+ }
+ hideKeyboard(textView);
+ updateFragment();
+ return true;
+ });
+ }
+
+ public void setEndDateInput(Context context, @NonNull EditText editText) {
+ editText.setOnEditorActionListener((textView, i, keyEvent) -> {
+ try {
+ endDate = DateHelper.dateConvert(textView.getText().toString());
+ } catch (Exception e) {
+ endDate = DateHelper.DEFAULT_END;
+ if (!textView.getText().toString().equals("")) {
+ Toast.makeText(context, "Invalid data format", Toast.LENGTH_SHORT).show();
+ }
+ }
+ hideKeyboard(textView);
+ updateFragment();
+ return true;
+ });
+ }
+
+ public void setIds(@NonNull long[] ids) {
+ this.ids = ids;
+ updateFragment();
+ }
+
+ public static void hideKeyboard(View v) {
+ InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ }
+ }
+
+ public void setChartFragment(ChartFragment chartFragment) {
+ this.chartFragment = chartFragment;
+ updateFragment();
+ }
+
+ private void updateFragment() {
+ chartFragment.setPeriod(beginDate, endDate);
+ Set whiteList = null;
+ if (ids != null) {
+ whiteList = new HashSet<>(ids.length);
+ for (long id : ids) {
+ whiteList.add(id);
+ }
+ }
+ chartFragment.setWhiteIdList(whiteList);
+ chartFragment.resetData();
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/charts/LineChartController.java b/Spender/app/src/main/java/com/project/spender/charts/LineChartController.java
new file mode 100644
index 0000000..87ec916
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/charts/LineChartController.java
@@ -0,0 +1,140 @@
+package com.project.spender.charts;
+
+import android.util.Log;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+
+import com.github.mikephil.charting.charts.LineChart;
+import com.github.mikephil.charting.components.AxisBase;
+import com.github.mikephil.charting.components.Legend;
+import com.github.mikephil.charting.components.XAxis;
+import com.github.mikephil.charting.data.Entry;
+import com.github.mikephil.charting.data.LineData;
+import com.github.mikephil.charting.data.LineDataSet;
+import com.github.mikephil.charting.formatter.ValueFormatter;
+import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
+import com.project.spender.data.entities.Tag;
+import com.project.spender.data.entities.TagWithSumAndDate;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * View controller for line chart
+ */
+public class LineChartController extends UpdatableChartController> {
+
+ private final LineChart lineChart;
+
+ public LineChartController(LifecycleOwner owner, LineChart lineChart) {
+ this.lineChart = lineChart;
+ this.owner = owner;
+ lineChartSetup();
+ }
+
+ private void lineChartSetup() {
+ lineChart.getDescription().setEnabled(false);
+
+ lineChart.setTouchEnabled(true);
+ lineChart.setDragEnabled(true);
+ lineChart.setScaleEnabled(true);
+ lineChart.setDrawGridBackground(false);
+
+ Legend legend = lineChart.getLegend();
+ legend.setVerticalAlignment(Legend.LegendVerticalAlignment.TOP);
+ legend.setHorizontalAlignment(Legend.LegendHorizontalAlignment.RIGHT);
+ legend.setOrientation(Legend.LegendOrientation.VERTICAL);
+ legend.setDrawInside(false);
+
+ ValueFormatter valueFormatter = new ValueFormatter() {
+
+ private final SimpleDateFormat mFormat = new SimpleDateFormat("dd MMM", Locale.ROOT);
+
+ @Override
+ public String getAxisLabel(float value, AxisBase axis) {
+ long millis = TimeUnit.HOURS.toMillis((long)value);
+ return mFormat.format(millis);
+ }
+ };
+
+ XAxis xAxis = lineChart.getXAxis();
+ xAxis.setGranularity(24f);
+ xAxis.setValueFormatter(valueFormatter);
+ xAxis.setDrawAxisLine(true);
+ xAxis.setTextSize(10f);
+ xAxis.setDrawGridLines(true);
+ xAxis.setCenterAxisLabels(true);
+ xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
+
+ lineChart.getAxisRight().setEnabled(false);
+ }
+
+ /**
+ * @param tagsWithSum список отсортированный по tag.id
+ */
+ @Override
+ protected void setData(List tagsWithSum) {
+ if (tagsWithSum.isEmpty()) {
+ return;
+ }
+
+ List dataSets = new ArrayList<>();
+
+ List entries = new ArrayList<>();
+
+
+ TagWithSumAndDate endTag = new TagWithSumAndDate();
+ tagsWithSum.add(endTag);
+
+ Tag currentTag = tagsWithSum.get(0).tag;
+
+ for (TagWithSumAndDate tagWithSumAndDate : tagsWithSum)
+ {
+ if (whiteIdList != null
+ && tagWithSumAndDate.tag.getId() != 0
+ && !whiteIdList.contains(tagWithSumAndDate.tag.getId())) {
+ continue;
+ }
+
+ if (currentTag.getId() != tagWithSumAndDate.tag.getId()) {
+
+ LineDataSet data = new LineDataSet(entries, currentTag.getName());
+ data.setCircleColor(currentTag.getColor());
+ data.setColor(currentTag.getColor());
+ dataSets.add(data);
+ entries = new ArrayList<>();
+ currentTag = tagWithSumAndDate.tag;
+ }
+
+ if (tagWithSumAndDate.tag.getId() == 0) {
+ continue;
+ }
+
+ Calendar calendar = Calendar.getInstance();
+ try {
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss", Locale.ROOT);
+ calendar.setTime(format.parse(tagWithSumAndDate.date));
+ } catch (ParseException e) {
+ Log.wtf("PARSER", "Cannot parse date from string. Exception: " + e.getMessage());
+ }
+
+ entries.add(new Entry(TimeUnit.MILLISECONDS.toHours(calendar.getTimeInMillis()), tagWithSumAndDate.sum/100f));
+ }
+
+ LineData data = new LineData(dataSets);
+ lineChart.setData(data);
+ lineChart.invalidate();
+ }
+
+ public void invalidate() {
+ lineChart.invalidate();
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/charts/PieChartController.java b/Spender/app/src/main/java/com/project/spender/charts/PieChartController.java
new file mode 100644
index 0000000..e4da06f
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/charts/PieChartController.java
@@ -0,0 +1,74 @@
+package com.project.spender.charts;
+
+import android.graphics.Color;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+
+import com.github.mikephil.charting.charts.PieChart;
+import com.github.mikephil.charting.data.PieData;
+import com.github.mikephil.charting.data.PieDataSet;
+import com.github.mikephil.charting.data.PieEntry;
+import com.github.mikephil.charting.formatter.PercentFormatter;
+import com.project.spender.data.entities.TagWithSum;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * View controller for pie and donut charts
+ */
+public class PieChartController extends UpdatableChartController> {
+ private final PieChart pieChart;
+
+ private final int speed = 1400;
+
+ public PieChartController(LifecycleOwner owner, PieChart pieChart) {
+ this.pieChart = pieChart;
+ this.owner = owner;
+ pieChart.setUsePercentValues(true);
+ pieChart.getDescription().setEnabled(false);
+ pieChart.setEntryLabelTextSize(18f);
+ pieChart.setEntryLabelColor(Color.BLACK);
+ pieChart.getLegend().setEnabled(false);
+ }
+
+ @Override
+ protected void setData(List tagsWithSum) {
+ List entries = new ArrayList<>();
+ List colors = new ArrayList<>();
+
+ for (TagWithSum tws : tagsWithSum) {
+ if (whiteIdList != null && !whiteIdList.contains(tws.tag.getId())) {
+ continue;
+ }
+ entries.add(new PieEntry(tws.sum, tws.tag.getName()));
+ colors.add(tws.tag.getColor());
+ }
+
+ PieDataSet dataSet = new PieDataSet(entries, "Tags");
+ dataSet.setColors(colors);
+ dataSet.setSliceSpace(3f);
+
+ PieData data = new PieData(dataSet);
+ data.setValueTextSize(20f);
+ data.setValueFormatter(new PercentFormatter(pieChart));
+
+ pieChart.setData(data);
+ pieChart.invalidate();
+ }
+
+ public void animate() {
+ pieChart.animateXY(speed, speed);
+ }
+
+ public void drawHole(boolean enable) {
+ pieChart.setDrawHoleEnabled(enable);
+ }
+
+ public void invalidate() {
+ pieChart.invalidate();
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/charts/StackedBarChartController.java b/Spender/app/src/main/java/com/project/spender/charts/StackedBarChartController.java
new file mode 100644
index 0000000..ca1320e
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/charts/StackedBarChartController.java
@@ -0,0 +1,168 @@
+package com.project.spender.charts;
+
+import android.util.Log;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+
+import com.github.mikephil.charting.charts.BarChart;
+import com.github.mikephil.charting.components.AxisBase;
+import com.github.mikephil.charting.components.Legend;
+import com.github.mikephil.charting.components.XAxis;
+import com.github.mikephil.charting.data.BarData;
+import com.github.mikephil.charting.data.BarDataSet;
+import com.github.mikephil.charting.data.BarEntry;
+import com.github.mikephil.charting.formatter.StackedValueFormatter;
+import com.github.mikephil.charting.formatter.ValueFormatter;
+import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
+import com.project.spender.data.entities.TagWithSumAndDate;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * View controller for bar chart
+ */
+public class StackedBarChartController extends UpdatableChartController> {
+ private final BarChart barChart;
+
+ private final int speed = 1400;
+
+ private List dates;
+
+ public StackedBarChartController(LifecycleOwner owner, BarChart barChart) {
+ this.barChart = barChart;
+ this.owner = owner;
+ StackedBarChartSetup();
+ }
+
+ private void StackedBarChartSetup() {
+ barChart.getDescription().setEnabled(false);
+ barChart.getLegend().setEnabled(false);
+ barChart.setMaxVisibleValueCount(60);
+ barChart.setPinchZoom(false);
+
+ barChart.setDrawGridBackground(false);
+ barChart.setDrawBarShadow(false);
+
+ barChart.setDrawValueAboveBar(false);
+ barChart.setHighlightFullBarEnabled(false);
+ barChart.getAxisRight().setEnabled(false);
+
+ ValueFormatter valueFormatter = new ValueFormatter() {
+
+ private final SimpleDateFormat mFormat = new SimpleDateFormat("dd MMM", Locale.ROOT);
+
+ @Override
+ public String getAxisLabel(float value, AxisBase axis) {
+ int i = (int) value;
+ String val = "";
+ if (dates.size() > i) {
+ val = mFormat.format(dates.get(i).getTime());
+ }
+ return val;
+ }
+ };
+
+ XAxis xAxis = barChart.getXAxis();
+ xAxis.setGranularity(1f);
+ xAxis.setValueFormatter(valueFormatter);
+
+ Legend legend = barChart.getLegend();
+ legend.setVerticalAlignment(Legend.LegendVerticalAlignment.BOTTOM);
+ legend.setHorizontalAlignment(Legend.LegendHorizontalAlignment.RIGHT);
+ legend.setOrientation(Legend.LegendOrientation.HORIZONTAL);
+ legend.setDrawInside(false);
+ }
+
+
+ private Calendar getTagDate(TagWithSumAndDate tagWSD) throws ParseException {
+ Calendar tagTime = Calendar.getInstance();
+ tagTime.setTime(format.parse(tagWSD.date));
+ return tagTime;
+ }
+
+ /**
+ * @param data отсортирован по дате
+ */
+ @Override
+ protected void setData(List data) {
+ if (data.isEmpty()) {
+ return;
+ }
+
+ if (whiteIdList != null) {
+ List clearData = new ArrayList<>();
+ for (TagWithSumAndDate tagWithSumAndDate : data) {
+ if (whiteIdList.contains(tagWithSumAndDate.tag.getId())) {
+ clearData.add(tagWithSumAndDate);
+ }
+ }
+ data = clearData;
+ }
+ List dataSets = new ArrayList<>();
+ dates = new ArrayList<>();
+
+ int num = 0;
+ for (ListIterator iter = data.listIterator(); iter.hasNext();) {
+ Calendar time = Calendar.getInstance();
+ TagWithSumAndDate currTagWSD = iter.next();
+ try {
+ time = getTagDate(currTagWSD);
+ } catch (ParseException e) {
+ Log.e("PARSER", "Parser error", e);
+ }
+ dates.add(time);
+ List values = new ArrayList<>();
+ List colors = new ArrayList<>();
+ values.add(currTagWSD.sum/100f);
+ colors.add(currTagWSD.tag.getColor());
+ while (iter.hasNext()) {
+ currTagWSD = iter.next();
+ try {
+ if (getTagDate(currTagWSD).getTimeInMillis() != time.getTimeInMillis()) {
+ iter.previous();
+ break;
+ }
+ } catch (ParseException e) {
+ Log.e("PARSER", "Parser error", e);
+ }
+ values.add(currTagWSD.sum/100f);
+ colors.add(currTagWSD.tag.getColor());
+ }
+
+ float[] arrValues = new float[values.size()];
+ int i = 0;
+ for (Float v : values) {
+ arrValues[i++] = v;
+ }
+ BarEntry barEntry = new BarEntry(num++, arrValues);
+ BarDataSet dataSet = new BarDataSet(Collections.singletonList(barEntry), data.toString());
+ dataSet.setColors(colors);
+ dataSets.add(dataSet);
+ }
+ BarData barData = new BarData(dataSets);
+ barData.setValueFormatter(new StackedValueFormatter(false,"",1));
+ barChart.setData(barData);
+ barChart.setFitBars(true);
+ barChart.invalidate();
+ }
+
+ public void animate() {
+ barChart.animateXY(speed, speed);
+ }
+
+
+ public void invalidate() {
+ barChart.invalidate();
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/charts/UpdatableChartController.java b/Spender/app/src/main/java/com/project/spender/charts/UpdatableChartController.java
new file mode 100644
index 0000000..f33273a
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/charts/UpdatableChartController.java
@@ -0,0 +1,38 @@
+package com.project.spender.charts;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+
+import com.project.spender.data.entities.TagWithSum;
+import com.project.spender.data.entities.TagWithSumAndDate;
+
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Abstract class for view controller for updatable charts
+ */
+public abstract class UpdatableChartController {
+ protected final static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss", Locale.ROOT);
+ private LiveData dataSource;
+ private Observer observer = this::setData;
+ protected LifecycleOwner owner;
+ protected Set whiteIdList;
+
+ public void setDataSource(LiveData data) {
+ if (dataSource != null) {
+ dataSource.removeObserver(observer);
+ }
+ dataSource = data;
+ dataSource.observe(owner, observer);
+ }
+
+ protected abstract void setData(T chartData);
+
+ public void setWhiteIdList(Set whiteIdList) {
+ this.whiteIdList = whiteIdList;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/CheckListHolder.java b/Spender/app/src/main/java/com/project/spender/controllers/CheckListHolder.java
new file mode 100644
index 0000000..272ff5f
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/CheckListHolder.java
@@ -0,0 +1,315 @@
+package com.project.spender.controllers;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.activities.CheckShowActivity;
+import com.project.spender.adapters.ItemAdapter;
+import com.project.spender.adapters.ListAdapter;
+import com.project.spender.data.entities.CheckWithProducts;
+import com.project.spender.data.entities.Product;
+import com.project.spender.data.entities.Tag;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import static com.project.spender.controllers.DateHelper.dateConvert;
+
+public class CheckListHolder {
+
+ @Inject protected ChecksRoller checksRoller;
+
+ @NonNull private String regEx;
+ @NonNull private String begin;
+ @NonNull private String end;
+ private List list;
+ private List productList;
+ private boolean isProductMode = false;
+ private ListView listView;
+ private List tags;
+ private int chosenPos;
+ private ListAdapter checksAdapter;
+ private ItemAdapter productsAdapter;
+ private AdapterView.OnItemClickListener checkListener;
+ private LifecycleOwner owner;
+
+
+ public CheckListHolder(ListView listView, Context context, TextView textView) {
+ App.getComponent().inject(this);
+
+ begin = DateHelper.DEFAULT_BEGIN;
+ end = DateHelper.DEFAULT_END;
+ regEx = "%%";
+ list = new ArrayList<>();
+ productList = new ArrayList<>();
+
+ textView.setVisibility(View.INVISIBLE);
+ owner = (LifecycleOwner) context;
+
+ this.listView = listView;
+ checksAdapter = new ListAdapter(context, list);
+ productsAdapter = new ItemAdapter(context, productList, null);
+ listView.setAdapter(checksAdapter);
+ checkListener = (adapterView, view, i, l) -> {
+ CheckWithProducts check = list.get(i);
+ Intent intent = new Intent(context, CheckShowActivity.class);
+ intent.putParcelableArrayListExtra("products",
+ (ArrayList) check.getProducts());
+ intent.putExtra("check id", check.getCheck().getId());
+ context.startActivity(intent);
+ };
+ listView.setOnItemClickListener(checkListener);
+
+ updateState();
+ System.out.println(checksRoller);
+ }
+
+ public void chooseItem(int index) {
+ chosenPos = index;
+ }
+
+ private void addTagsForCheck(long[] ids) {
+ if (chosenPos == -1) {
+ return;
+ }
+ addTags(chosenPos, ids);
+ }
+
+ private void removeTagsForCheck(long[] ids) {
+ if (chosenPos == -1) {
+ return;
+ }
+ removeTags(chosenPos, ids);
+ }
+
+ public void changeMode() {
+ isProductMode = !isProductMode;
+ chosenPos = -1;
+ if (isProductMode) {
+ listView.setAdapter(productsAdapter);
+ listView.setOnItemClickListener((adapterView, view, i, l) -> {});
+ } else {
+ listView.setAdapter(checksAdapter);
+ listView.setOnItemClickListener(checkListener);
+ }
+ updateState();
+ }
+
+ public void setBegin(@NonNull String begin) {
+ try {
+ this.begin = dateConvert(begin);
+ } catch (ParseException e) {
+ this.begin = DateHelper.DEFAULT_BEGIN;
+ if (!begin.equals("")) {
+ throw new IllegalArgumentException();
+ }
+ }
+ updateState();
+ }
+
+ public void setEnd(@NonNull String end) {
+ try {
+ this.end = dateConvert(end);
+ } catch (ParseException e) {
+ this.end = DateHelper.DEFAULT_END;
+ if (!end.equals("")) {
+ throw new IllegalArgumentException();
+ }
+ }
+ updateState();
+ }
+
+ public void setSubstring( String substring) {
+ Log.i(ChecksRoller.LOG_TAG, substring + "");
+ regEx = "%" + substring + "%";
+ updateState();
+ }
+
+ private void updateState() {
+ if (isProductMode) {
+ updateStateProduct();
+ for (Product p : productList) {
+ Log.i(ChecksRoller.LOG_TAG, "" + p.getName());
+ }
+ Log.i(ChecksRoller.LOG_TAG, productList.size() + " is product");
+ } else {
+ updateStateCheck();
+ for (CheckWithProducts p : list) {
+ Log.i(ChecksRoller.LOG_TAG, "" + p.getCheck().getName());
+ }
+
+ Log.i(ChecksRoller.LOG_TAG, list.size() + " is check");
+ }
+
+ listView.invalidateViews();
+ }
+
+ //its terrible (todo) fix it
+ public void setTags(@NonNull long[] ids) {
+ if (ids.length == 0){
+ tags = null;
+ return;
+ }
+ tags = new ArrayList<>();
+ for (long id : ids) {
+ Log.i(ChecksRoller.LOG_TAG, id + "");
+ tags.add(id);
+ }
+ updateState();
+ }
+
+ public void addTagsForItem(long[] ids) {
+ if (chosenPos == -1) {
+ return;
+ }
+
+ if (isProductMode) {
+ Product product = productList.get(chosenPos);
+ for (long i : ids) {
+ checksRoller.insertTagForProductById(i, product.getId());
+ }
+ } else {
+ addTagsForCheck(ids);
+ }
+
+ chosenPos = -1;
+ }
+
+ public void removeTagsForItem(long[] ids) {
+ if (chosenPos == -1) {
+ return;
+ }
+
+ if (isProductMode) {
+ Product product = productList.get(chosenPos);
+ for (long i : ids) {
+ checksRoller.deleteTagForProduct(i, product.getId());
+ }
+ } else {
+ removeTagsForCheck(ids);
+ }
+ chosenPos = -1;
+ }
+
+ private void removeCheck(int i) {
+ checksRoller.deleteCheck(list.get(i).getCheck().getId());
+ list.remove(i);
+ }
+
+ private void removeProduct(int i) {
+ checksRoller.deleteProduct(productList.get(i).getId());
+ productList.remove(i);
+ }
+
+ public void removeItem(int i) {
+ if (isProductMode) {
+ removeProduct(i);
+ } else {
+ removeCheck(i);
+ }
+ listView.invalidateViews();
+ }
+
+ public boolean getMode() {
+ return isProductMode;
+ }
+
+ private synchronized void addIfContain(List list1, List tags1, T o) {
+ for (Tag tag : tags1) {
+ if (tags.contains(tag.getId())) {
+ list1.add(o);
+ break;
+ }
+ }
+ listView.invalidateViews();
+ }
+
+ private synchronized void updateStateCheckByList(List list1) {
+ if (tags == null) {
+ list.clear();
+ list.addAll(list1);
+ listView.invalidateViews();
+ return;
+ }
+ for (CheckWithProducts check : list1) {
+ LiveData> tagList = checksRoller.getAppDatabase().getCheckDao().getTagsByCheckId(check.getCheck().getId());
+// Log.i(ChecksRoller.LogG_TAG, );
+ tagList.observe(owner, tags -> addIfContain(list, tags, check));
+ }
+ }
+
+ private synchronized void updateStateProductByList(List list1) {
+ if (tags == null) {
+ productList.clear();
+ productList.addAll(list1);
+ listView.invalidateViews();
+ return;
+ }
+ for (Product product: list1) {
+ LiveData> tagList = checksRoller.getAppDatabase().getCheckDao().getTagsByProductId(product.getId());
+ tagList.observe(owner, tags -> addIfContain(productList, tags, product));
+ }
+ }
+
+ private void updateStateCheck() {
+ list.clear();
+ LiveData> list1 = checksRoller.findChecksByTimePeriodAndRegEx(begin, end, regEx);
+ list1.observe(owner, this::updateStateCheckByList);
+ }
+
+ private void updateStateProduct() {
+ productList.clear();
+ LiveData> list1 = checksRoller.getAppDatabase().getCheckDao().getProductsByRegEx(regEx);
+ list1.observe(owner, this::updateStateProductByList);
+ }
+
+ private void addTags(int position, long[] tagIds) {
+ for (long id : tagIds) {
+ if (isProductMode) {
+ addTagForProduct(position, id);
+ } else {
+ addTagForCheck(position, id);
+ }
+ }
+ }
+
+ private void addTagForCheck(int position, long tagId) {
+ for (Product product : list.get(position).getProducts()) {
+ checksRoller.insertTagForProductById(tagId, product.getId());
+ }
+ }
+
+ private void addTagForProduct(int position, long tagId) {
+ checksRoller.insertTagForProductById(tagId, productList.get(position).getId());
+ }
+
+ private void removeTags(int position, long[] tagIds) {
+ for (long id : tagIds) {
+ removeTag(position, id);
+ }
+ }
+
+ private void removeTag(int position, long tagId) {
+ for (Product product : list.get(position).getProducts()) {
+ checksRoller.deleteTagForProduct(tagId, product.getId());
+ }
+ }
+
+ public List getList() {
+ return list;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/CheckShowHelper.java b/Spender/app/src/main/java/com/project/spender/controllers/CheckShowHelper.java
new file mode 100644
index 0000000..e53b4fc
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/CheckShowHelper.java
@@ -0,0 +1,156 @@
+package com.project.spender.controllers;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+
+import com.mattyork.colours.Colour;
+import com.project.spender.adapters.ItemAdapter;
+import com.project.spender.data.entities.Product;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Class to control CheckShow activity view
+ */
+public class CheckShowHelper {
+
+ @Inject protected ChecksRoller checksRoller;
+ private List products;
+ private HashSet productsForAction;
+ private ListView listView;
+ private String substring;
+ private long checkId;
+
+ private LifecycleOwner owner;
+
+ public static final int SELECTED_ITEM = Colour.pastelGreenColor();
+ public static final int UNSELECTED_ITEM = Color.WHITE;
+
+ public CheckShowHelper(Context context, ListView listView, Intent intent) {
+ App.getComponent().inject(this);
+
+ productsForAction = new HashSet<>();
+ owner = (LifecycleOwner) context;
+ this.listView = listView;
+
+ products = new ArrayList<>();
+ checkId = intent.getLongExtra("check id", -1);
+ substring = "%%";
+
+ listView.setAdapter(new ItemAdapter(context, products, productsForAction));
+
+ listView.setOnItemClickListener((parent, view, position, id) -> {
+ if (productsForAction.contains(position)) {
+ productsForAction.remove(position);
+ view.setBackgroundColor(UNSELECTED_ITEM);
+ } else {
+ productsForAction.add(position);
+ view.setBackgroundColor(SELECTED_ITEM);
+ }
+ });
+
+ update();
+ }
+
+ /**
+ * Set up listener for given EditText to save its content to search parameter
+ */
+ public void setInput(EditText editText) {
+ editText.setOnEditorActionListener((v, actionId, event) -> {
+ substring = "%" + editText.getText().toString() + "%";
+ update();
+ return true;
+ });
+
+ }
+
+ private void update() {
+ LiveData> list = checksRoller.getAppDatabase().getCheckDao().getProductByRegEx(substring, checkId);
+ list.observe(owner, this::updateProducts);
+ }
+
+ private void updateProducts(List products) {
+ this.products.clear();
+ this.products.addAll(products);
+ listView.invalidateViews();
+ }
+
+ private View getItemView(int position) {
+ final int firstListItemPosition = listView.getFirstVisiblePosition();
+ final int lastListItemPosition = firstListItemPosition + listView.getChildCount() - 1;
+
+ if (position < firstListItemPosition || position > lastListItemPosition ) {
+ return listView.getAdapter().getView(position, null, listView);
+ } else {
+ final int childIndex = position - firstListItemPosition;
+ return listView.getChildAt(childIndex);
+ }
+ }
+
+ private void unColorSelected() {
+ for (int i : productsForAction) {
+ System.out.println(i);
+ getItemView(i).setBackgroundColor(UNSELECTED_ITEM);
+ }
+ listView.invalidateViews();
+ }
+
+ private void clearSelectedSet() {
+ productsForAction.clear();
+ }
+
+ private void clearSelected() {
+ unColorSelected();
+ clearSelectedSet();
+ }
+
+ /**
+ * Adds tags for selected products
+ */
+ public void addTags(long[] tags) {
+ for (int i : productsForAction) {
+ for (long j : tags) {
+ checksRoller.insertTagForProductById(j, products.get(i).getId());
+ }
+ }
+ clearSelected();
+ }
+
+ /**
+ * Remove tags for selected products
+ */
+ public void removeTags(long[] tags) {
+ for (int i : productsForAction) {
+ for (long j : tags) {
+ checksRoller.deleteTagForProduct(j, products.get(i).getId());
+ }
+ }
+ clearSelected();
+ }
+
+ /**
+ * Removes selected products
+ */
+ public void removeProducts() {
+ unColorSelected();
+ for (int i : productsForAction) {
+ checksRoller.deleteProduct(products.get(i).getId());
+ products.remove(i);
+ }
+ clearSelectedSet();
+ listView.invalidateViews();
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/DateHelper.java b/Spender/app/src/main/java/com/project/spender/controllers/DateHelper.java
new file mode 100644
index 0000000..72ea6e7
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/DateHelper.java
@@ -0,0 +1,30 @@
+package com.project.spender.controllers;
+
+import android.util.Log;
+
+import com.project.spender.roller.ChecksRoller;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+/**
+ * Class for generalizing dates usage
+ */
+public abstract class DateHelper {
+
+ public static final String DEFAULT_BEGIN = "1999-03-22";
+ public static final String DEFAULT_END = "now";
+
+ /**
+ * Converts date from DD.MM.YYYY to YYYY-MM-DD
+ */
+ public static String dateConvert(String date) throws IllegalArgumentException, ParseException {
+ SimpleDateFormat formatInput = new SimpleDateFormat("dd.MM.yyyy");
+ SimpleDateFormat formatResult = new SimpleDateFormat("yyyy-MM-dd");
+
+ Log.i(ChecksRoller.LOG_TAG, formatResult.format(formatInput.parse(date)));
+ return formatResult.format(formatInput.parse(date));
+
+
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/HistoryListHolder.java b/Spender/app/src/main/java/com/project/spender/controllers/HistoryListHolder.java
new file mode 100644
index 0000000..0d5b58c
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/HistoryListHolder.java
@@ -0,0 +1,72 @@
+package com.project.spender.controllers;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.adapters.HistoryItemAdapter;
+import com.project.spender.data.CheckStatus;
+import com.project.spender.data.entities.CheckWithProducts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for storage check requests statuses
+ */
+public class HistoryListHolder {
+
+ private ListView listView;
+ private List list;
+ private TextView info;
+
+ public HistoryListHolder() {
+ list = new ArrayList<>();
+ }
+
+ /**
+ * Adds list view for statuses list
+ */
+ public void setListView(Context context, ListView listView, TextView textView) {
+ this.listView = listView;
+ info = textView;
+ listView.setAdapter(new HistoryItemAdapter(context, list));
+ infoSet();
+ }
+
+ /**
+ * Ads new checks status
+ */
+ public void add(CheckStatus checkStatus) {
+ list.add(checkStatus);
+ upgrade();
+ }
+
+ /**
+ * Updates list view if it not null
+ */
+ public void update() {
+ if (listView != null) {
+ listView.invalidateViews();
+ }
+ }
+
+ private void infoSet() {
+ if (list.size() == 0) {
+ info.setVisibility(View.VISIBLE);
+ } else {
+ info.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ private void upgrade() {
+ if (listView != null) {
+ infoSet();
+ listView.invalidate();
+ }
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/TagChoiceHelper.java b/Spender/app/src/main/java/com/project/spender/controllers/TagChoiceHelper.java
new file mode 100644
index 0000000..c554e91
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/TagChoiceHelper.java
@@ -0,0 +1,83 @@
+package com.project.spender.controllers;
+
+import android.content.Context;
+import android.content.Intent;
+import android.widget.ListView;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.adapters.TagChoiceAdapter;
+import com.project.spender.data.entities.Tag;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import static com.project.spender.controllers.CheckShowHelper.SELECTED_ITEM;
+import static com.project.spender.controllers.CheckShowHelper.UNSELECTED_ITEM;
+
+/**
+ * Class to control tag choice process
+ */
+public class TagChoiceHelper {
+
+ @Inject protected ChecksRoller checksRoller;
+
+ private List clickedTags;
+ private List tags;
+ private ListView listView;
+
+ public static final String TAG_ID_LIST = "vlad molodec";
+
+ /**
+ * Creates new instance with given list view for all tags list, using context as owner
+ */
+ public TagChoiceHelper(Context context, ListView listView) {
+ App.getComponent().inject(this);
+
+ this.listView = listView;
+ clickedTags = new ArrayList<>();
+ tags = new ArrayList<>();
+
+ LiveData> ts = checksRoller.getAppDatabase().getCheckDao().getAllTags();
+ ts.observe((LifecycleOwner) context, this::setTags);
+
+ listView.setOnItemClickListener((parent, view, position, id) -> {
+ Tag current = tags.get(position);
+ if (clickedTags.contains(current)) {
+ clickedTags.remove(current);
+ view.setBackgroundColor(UNSELECTED_ITEM);
+ } else {
+ clickedTags.add(current);
+ view.setBackgroundColor(SELECTED_ITEM);
+ }
+ });
+ listView.setAdapter(new TagChoiceAdapter(context, tags, clickedTags));
+
+ }
+
+ private void setTags(List list) {
+ tags.clear();
+ tags.addAll(list);
+ listView.invalidateViews();
+
+ }
+
+ /**
+ * Created return intent with extra tags list in it
+ */
+ public Intent resultIntent() {
+ Intent intent = new Intent();
+ long[] ids = new long[clickedTags.size()];
+ for (int i = 0; i < clickedTags.size(); i++) {
+ ids[i] = clickedTags.get(i).getId();
+ }
+ intent.putExtra(TAG_ID_LIST, ids);
+ return intent;
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/TagListHelper.java b/Spender/app/src/main/java/com/project/spender/controllers/TagListHelper.java
new file mode 100644
index 0000000..f2a14c5
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/TagListHelper.java
@@ -0,0 +1,88 @@
+package com.project.spender.controllers;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+
+import com.project.spender.adapters.TagChoiceAdapter;
+import com.project.spender.data.entities.Tag;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Class to control all tags showing activity
+ */
+public class TagListHelper {
+
+ @Inject protected ChecksRoller checksRoller;
+
+ private ListView listView;
+ private List tags;
+ private TextView info;
+
+ /**
+ * Creates new instance
+ * @param context owner
+ * @param listView view for tags list
+ * @param textView info showing in empty list case
+ */
+ public TagListHelper(Context context, ListView listView, TextView textView) {
+ App.getComponent().inject(this);
+
+ this.listView = listView;
+ info = textView;
+ tags = new ArrayList<>();
+ listView.setAdapter(new TagChoiceAdapter(context, tags, null));
+ update((LifecycleOwner) context);
+ App.getComponent().inject(this);
+ }
+
+ /**
+ * Removes element with given index
+ */
+ public void remove(int i) {
+ checksRoller.deleteTag(tags.get(i).getId());
+ }
+
+ /**
+ * Updates all tags list
+ */
+ public void update(LifecycleOwner owner) {
+ LiveData> list = checksRoller.getAppDatabase().getCheckDao().getAllTags();
+ list.observe(owner, this::updateList);
+ }
+
+ /**
+ * Replaces tags list with given list.
+ * If its empty, shows info
+ */
+ private void updateList(List list) {
+ tags.clear();
+ tags.addAll(list);
+ if (tags.size() == 0) {
+ info.setVisibility(View.VISIBLE);
+ } else {
+ info.setVisibility(View.INVISIBLE);
+ }
+ listView.invalidateViews();
+ }
+
+ /**
+ * Returns default tag view parameters
+ */
+ public static LinearLayout.LayoutParams tagParams() {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(50, 30);
+ params.setMargins(5, 0, 5, 0);
+ return params;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/controllers/TagStateHolder.java b/Spender/app/src/main/java/com/project/spender/controllers/TagStateHolder.java
new file mode 100644
index 0000000..13687ec
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/controllers/TagStateHolder.java
@@ -0,0 +1,102 @@
+package com.project.spender.controllers;
+
+import android.graphics.Color;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.github.mikephil.charting.utils.ColorTemplate;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import javax.inject.Inject;
+
+/**
+ * Class for storage new tag and creating
+ */
+public class TagStateHolder {
+
+ @Inject protected ChecksRoller checksRoller;
+ private static final List DEFAULT_COLOR = new ArrayList<>();
+
+ static {
+ for (int i : ColorTemplate.VORDIPLOM_COLORS) {
+ DEFAULT_COLOR.add(i);
+ }
+
+ for (int i : ColorTemplate.MATERIAL_COLORS) {
+ DEFAULT_COLOR.add(i);
+ }
+
+ for (int i : ColorTemplate.JOYFUL_COLORS) {
+ DEFAULT_COLOR.add(i);
+ }
+
+ for (int i : ColorTemplate.LIBERTY_COLORS) {
+ DEFAULT_COLOR.add(i);
+ }
+
+ for (int i : ColorTemplate.COLORFUL_COLORS) {
+ DEFAULT_COLOR.add(i);
+ }
+
+ for (int i : ColorTemplate.PASTEL_COLORS) {
+ DEFAULT_COLOR.add(i);
+ }
+
+
+ }
+
+ private String name;
+ @Nullable private String regEx;
+ private Integer color;
+
+ public TagStateHolder() {
+ App.getComponent().inject(this);
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Trying to parse color from given string.
+ * If its possible saves it
+ */
+ public void setColor(String color) {
+ Log.i(ChecksRoller.LOG_TAG, color + "");
+ try {
+ this.color = Color.parseColor(color);
+ } catch (Exception e) {
+ this.color = null;
+ }
+ }
+
+ /**
+ * Saves given string as regular expression for auto add to products
+ */
+ public void setRegEx(String regEx) {
+ Log.i(ChecksRoller.LOG_TAG, "TRYING TO ADD NEW TAG " + regEx);
+ if (!regEx.equals("")) {
+ this.regEx = regEx;
+ }
+ }
+
+ /**
+ * Creates new tag with saved parameters
+ * If where no saved color, uses random one
+ */
+ public void createTag() {
+ if (color == null) {
+ Random random = new Random();
+ int index = random.nextInt(DEFAULT_COLOR.size());
+ color = DEFAULT_COLOR.get(index);
+ }
+
+ checksRoller.addTag(name, regEx, color);
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/AppDatabase.java b/Spender/app/src/main/java/com/project/spender/data/AppDatabase.java
new file mode 100644
index 0000000..5b07dce
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/AppDatabase.java
@@ -0,0 +1,32 @@
+package com.project.spender.data;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+
+import com.project.spender.data.entities.Check;
+import com.project.spender.data.entities.Product;
+import com.project.spender.data.entities.ProductTagJoin;
+import com.project.spender.data.entities.Tag;
+
+/**
+ * Основной класс по работе с базой данных.
+ *
+ * Пример создания:
+ * AppDatabase db = Room.databaseBuilder(getApplicationContext(),
+ * AppDatabase.class, "file_name").build();
+ *
+ * Чтобы разрешить взаимодействие главного потока и бд нужно добавить
+ * .allowMainThreadQueries() перед .build().
+ *
+ * Долго создается, поэтому нужно создавать в Application классе
+ * Пример: https://github.com/gonzalonm/RoomDemo/blob/master/app/src/main/java/com/lalosoft/roomdemo/App.java
+ *
+ */
+@Database(entities = {Check.class, Product.class, Tag.class, ProductTagJoin.class}, version = 6, exportSchema = false)
+public abstract class AppDatabase extends RoomDatabase {
+ /**
+ * Метод для получения класса для работы с бд.
+ * @return DAO (Data Access Objects) реализующий CheckDao.
+ */
+ public abstract CheckDao getCheckDao();
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/CheckDao.java b/Spender/app/src/main/java/com/project/spender/data/CheckDao.java
new file mode 100644
index 0000000..5433b03
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/CheckDao.java
@@ -0,0 +1,305 @@
+package com.project.spender.data;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Transaction;
+
+import com.project.spender.data.entities.Check;
+import com.project.spender.data.entities.CheckWithProducts;
+import com.project.spender.data.entities.Product;
+import com.project.spender.data.entities.ProductTagJoin;
+import com.project.spender.data.entities.ProductWithTags;
+import com.project.spender.data.entities.Tag;
+import com.project.spender.data.entities.TagWithSum;
+import com.project.spender.data.entities.TagWithSumAndDate;
+
+import java.util.List;
+
+/**
+ * Абстрактный класс описывает правила общения с бд.
+ * Умеет интегрироваться с LiveData (надо бы сделать)
+ * Без LiveData по умолчанию вызов любой операции в main потоке кидает Exception (зависит от бд см AppDatabase).
+ */
+@Dao
+public abstract class CheckDao {
+
+ // INSERT
+
+ /**
+ * Добавляет новый чек в бд. Если id > 0 и такой id уже есть, то кидает Exception.
+ * Не изменяет переданный объект.
+ *
+ * @param check добавляемы чек.
+ * @return новый id или 0 если элемент не был добавлен.
+ */
+ @Insert
+ public abstract long insertCheck(Check check);
+
+ /**
+ * Добавляет новый товар в бд. Если id > 0 и такой id уже есть, то кидает Exception.
+ * Если в таблице нет чека с id == check_id, то кидает Exception.
+ * Не изменяет переданный объект.
+ *
+ * @param product добавляемый товар.
+ * @return новый id или 0 если элемент не был добавлен.
+ */
+ @Insert
+ public abstract long insertProduct(Product product);
+
+ /**
+ * Добавляет тег. Ничего не делает если такой есть.
+ * Имя тега, как и id уникальный.
+ *
+ * @param tag добавляемый тег.
+ * @return новый id или 0 если такой есть.
+ */
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ public abstract long insertTag(Tag tag);
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ public abstract void insertProductTagJoin(ProductTagJoin productTagJoin);
+
+ /**
+ * Добавляет в бд чек со всеми товарами. Обновляет id всех добавленных объектов.
+ * В случае неудачи кидает Exception.
+ *
+ * @param checkWithProducts добавляемый чек с товарами.
+ */
+ @Transaction
+ public void insertCheckWithProducts(CheckWithProducts checkWithProducts) {
+ long newIndex = insertCheck(checkWithProducts.getCheck());
+ checkWithProducts.updateCheckId(newIndex);
+ for (Product product : checkWithProducts.getProducts()) {
+ product.setId(insertProduct(product));
+ }
+ }
+
+ /**
+ * Добавляет в бд тег для товара с данным id.
+ * Если тег есть, ничего не делает.
+ *
+ * @param tag добавляемый тег.
+ * @param productId id соответствующего товара.
+ */
+ @Transaction
+ public long insertTagForProduct(Tag tag, long productId) {
+ long tagId = (tag.getId() > 0) ? tag.getId() : insertTag(tag);
+ if (tagId <= 0) {
+ tagId = getTagId(tag.getName());
+ }
+ insertProductTagJoin(new ProductTagJoin(productId, tagId));
+ return tagId;
+ }
+
+ /**
+ * Adds tag for product by id
+ */
+ public void insertExistingTagForProduct(long tagId, long productId) {
+ insertProductTagJoin(new ProductTagJoin(productId, tagId));
+ }
+
+ /**
+ * Добавляет лист тегов для товара с данным id.
+ * Если тег есть, ничего не делает.
+ *
+ * @param tags добавляемый тег.
+ * @param productId id соответстующего товара.
+ */
+ @Transaction
+ public void insertTagsForProduct(List tags, long productId) {
+ for (Tag tag : tags) {
+ insertTagForProduct(tag, productId);
+ }
+ }
+
+ /**
+ * Добавляет продукт со списком тегов.
+ * В отличие от insertCheckWithProducts не обновляет id в переданных объектах (хз как лучше)
+ * @param productWithTags продукт с списком тегов
+ */
+ @Transaction
+ public void insertProductWithTags(ProductWithTags productWithTags) {
+ long productId = insertProduct(productWithTags.getProduct());
+ insertTagsForProduct(productWithTags.getTags(), productId);
+ }
+
+ // GET
+
+ /**
+ * Метод получает все товары из бд без информации о чеках.
+ *
+ * @return все товары
+ */
+ @Query("SELECT * FROM Product")
+ public abstract LiveData> getAllProducts();
+
+ /**
+ * Returns list of all tags
+ */
+ @Query("SELECT * FROM Tag")
+ public abstract LiveData> getAllTags();
+
+ @Transaction
+ @Query("SELECT * FROM `Check` WHERE name LIKE :exp")
+ public abstract LiveData> getCheckByRegEx(String exp);
+
+
+
+ /**
+ * Метод получает все чеки из бд без информации о товарах.
+ *
+ * @return все чеки
+ */
+ @Query("SELECT * FROM `Check`")
+ public abstract LiveData> getAllChecks();
+
+ /**
+ * Метод получает все чеки вместе со всеми товарами в них.
+ *
+ * @return все чеки с информацией о товарах
+ */
+ @Transaction
+ @Query("SELECT * FROM `Check`")
+ public abstract LiveData> getAll();
+
+ /**
+ * Возвращает id тега по имени.
+ *
+ * @param name имя тега.
+ * @return id тега или 0 если такого нет.
+ */
+ @Query("SELECT id FROM tag WHERE name = :name")
+ public abstract long getTagId(String name);
+
+ @Transaction
+ @Query("SELECT tag.id, tag.name, tag.color, tag.substring FROM tag INNER JOIN product_tag_join ON tag.id = tag_id WHERE product_id == :productId")
+ public abstract LiveData> getTagsByProductId(long productId);
+
+ @Transaction
+ @Query("SELECT DISTINCT tag.id, tag.name, tag.color, tag.substring FROM tag, product_tag_join, product WHERE tag.id = tag_id AND product_id == product.id AND check_id = :checkId")
+ public abstract LiveData> getTagsByCheckId(long checkId);
+
+ @Transaction
+ @Query("SELECT tag.id, tag.name, tag.color, tag.substring, SUM(product.sum) as sum " +
+ "FROM tag, product_tag_join, product " +
+ "WHERE tag.id = tag_id AND product_id == product.id " +
+ "GROUP BY tag.id")
+ public abstract LiveData> getTagsWithSum();
+
+ @Transaction
+ @Query("SELECT tag.id, tag.name, tag.color, tag.substring, SUM(product.sum) as sum " +
+ "FROM tag, product_tag_join, product, `check` " +
+ "WHERE tag.id = tag_id AND product_id == product.id AND product.check_id == `check`.id " +
+ "AND datetime(`check`.date) BETWEEN datetime(:start) AND datetime(:finish)" +
+ "GROUP BY tag.id")
+ public abstract LiveData> getTagsWithSumByData(String start, String finish);
+
+ /**
+ * Получает последний вставленный id.
+ *
+ * @return послединий id.
+ */
+ @Query("SELECT last_insert_rowid()")
+ public abstract long getLastId();
+
+ /**
+ * Список чеков по дате.
+ * Формат даты -- строка вида:
+ * 1 YYYY-MM-DD 2010-12-30
+ * 2 YYYY-MM-DD HH:MM 2010-12-30 12:10
+ * 3 YYYY-MM-DD HH:MM:SS.SSS 2010-12-30 12:10:04.100
+ * 4 MM-DD-YYYY HH:MM 30-12-2010 12:10
+ * 5 HH:MM 12:10
+ * 6 YYYY-MM-DDTHH:MM 2010-12-30T12:10
+ * 7 HH:MM:SS 12:10:01
+ * 8 YYYYMMDD HHMMSS 20101230 121001
+ * 9 now 2013-05-07
+ *
+ * @param start
+ * @param finish
+ * @return все чеки попадающие в отрезок.
+ */
+ @Transaction
+ @Query("SELECT * FROM `Check` WHERE datetime(date) BETWEEN datetime(:start) AND datetime(:finish)")
+ public abstract List getChecksByDate(String start, String finish);
+
+ @Transaction
+ @Query("SELECT tag.id, tag.name, tag.color, tag.substring, SUM(product.sum) as sum, date " +
+ "FROM tag, product_tag_join, product, `check` " +
+ "WHERE tag.id = tag_id AND product_id == product.id AND product.check_id == `check`.id " +
+ "GROUP BY tag.id, date " +
+ "ORDER BY tag.id")
+ public abstract LiveData> getTagsWithSumAndDate();
+
+ @Transaction
+ @Query("SELECT tag.id, tag.name, tag.color, tag.substring, SUM(product.sum) as sum, date " +
+ "FROM tag, product_tag_join, product, `check` " +
+ "WHERE tag.id = tag_id AND product_id == product.id AND product.check_id == `check`.id " +
+ "AND datetime(`check`.date) BETWEEN datetime(:start) AND datetime(:finish)" +
+ "GROUP BY tag.id, date " +
+ "ORDER BY tag.id")
+ public abstract LiveData> getTagsWithSumAndDateByDate(String start, String finish);
+
+ @Transaction
+ @Query("SELECT tag.id, tag.name, tag.color, tag.substring, SUM(product.sum) as sum, date " +
+ "FROM tag, product_tag_join, product, `check` " +
+ "WHERE tag.id = tag_id AND product_id == product.id AND product.check_id == `check`.id " +
+ "AND datetime(`check`.date) BETWEEN datetime(:start) AND datetime(:finish)" +
+ "GROUP BY tag.id, date " +
+ "ORDER BY date")
+ public abstract LiveData> getTagsWithSumAndDateByDateOderedByDate(String start, String finish);
+
+ @Transaction
+ @Query("SELECT * FROM `Check` WHERE datetime(date) BETWEEN datetime(:start) AND datetime(:finish)")
+ public abstract LiveData> getChecksWithProductsByDate(String start, String finish);
+
+
+
+ /**
+ * Trying to add little update
+ * Returns list of checks in given (as only russian say) period matching regular expression
+ */
+
+ @Transaction
+ @Query("SELECT * FROM `Check` WHERE name LIKE :regEx AND datetime(date) BETWEEN datetime(:start) AND datetime(:finish)")
+ public abstract LiveData> getChecksWithProductsByDateAndRegEx(String regEx, String start, String finish);
+
+ @Query("SELECT * FROM `Product` WHERE name LIKE :regEx ")
+ public abstract LiveData> getProductsByRegEx(String regEx);
+
+ @Query("SELECT * FROM Product WHERE name LIKE :exp AND check_id = :checkId")
+ public abstract LiveData> getProductByRegEx(String exp, long checkId);
+
+ // DELETE. Все зависимые объекты удаляются автоматически. Например все товары из чека.
+ // Теги являются независимыми, поэтому их иногда нужно чистить вручную.
+
+
+ @Query("DELETE FROM 'check'")
+ public abstract void deleteAll();
+
+ @Query("DELETE FROM `check` WHERE id = :id")
+ public abstract void deleteCheckById(long id);
+
+ @Query("DELETE FROM product WHERE id = :id")
+ public abstract void deleteProductById(long id);
+
+ @Query("DELETE FROM tag WHERE id = :id")
+ public abstract void deleteTagById(long id);
+
+ @Query("DELETE FROM tag WHERE name = :name")
+ public abstract void deleteTagByName(String name);
+
+ @Delete
+ public abstract void deleteTagProductRelation(ProductTagJoin productTagJoin);
+
+ /**
+ * Удаляет все неиспользуемые теги.
+ */
+ @Transaction
+ @Query("DELETE FROM tag WHERE id NOT IN (SELECT tag_id FROM product_tag_join)")
+ public abstract void deleteAllUnusedTags();
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/CheckStatus.java b/Spender/app/src/main/java/com/project/spender/data/CheckStatus.java
new file mode 100644
index 0000000..f28abc1
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/CheckStatus.java
@@ -0,0 +1,33 @@
+package com.project.spender.data;
+
+public class CheckStatus {
+
+ private String time;
+ private String status;
+ private int counter;
+
+ public CheckStatus(String time) {
+ this.time = time;
+ counter = 0;
+ }
+
+ public synchronized void incCounter() {
+ counter++;
+ }
+
+ public synchronized int getCounter() {
+ return counter;
+ }
+
+ public synchronized void settStatus(String status) {
+ this.status = status;
+ }
+
+ public synchronized String getStatus() {
+ return status;
+ }
+
+ public String getTime() {
+ return time;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/ScanResult.java b/Spender/app/src/main/java/com/project/spender/data/ScanResult.java
new file mode 100644
index 0000000..3010d2d
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/ScanResult.java
@@ -0,0 +1,104 @@
+package com.project.spender.data;
+
+import android.app.Activity;
+import android.util.Log;
+
+import com.project.spender.roller.ChecksRoller;
+
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for store result of code scanning
+ */
+public class ScanResult {
+
+ //(todo) come up with natural clear name
+ public static final int NOT_ENOUGH_DATA = -10;
+ public static final int WRONG_CODE = -20;
+
+ private final String fn;
+ private final String fd;
+ private final String fp;
+ private final String date;
+ private final String sum;
+
+ private List parseNumbers(String content) {
+ List res = new ArrayList<>();
+ System.out.println(content);
+ for (String i : content.split("[&|=|a-z]")) {
+ if (i.length() != 0) {
+ if (i.contains(".")) {
+ res.add(i.replace(".", ""));
+ } else {
+ res.add(i);
+ }
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Returns text representation of given result code
+ */
+ public static String explain(int code) {
+ switch (code) {
+ case Activity.RESULT_OK:
+ return "Scanned";
+ case NOT_ENOUGH_DATA:
+ return "Authorization required to continue";
+ case WRONG_CODE:
+ return "Content of the code does not match the format of checks codes";
+ default:
+ return "not scanned";
+ }
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public String getFd() {
+ return fd;
+ }
+
+ public String getFn() {
+ return fn;
+ }
+
+ public String getFp() {
+ return fp;
+ }
+
+ public String getSum() {
+ return sum;
+ }
+
+ /**
+ * Parses given string and creates new instance.
+ * If string can not be parsed as check info throws exception
+ */
+ public ScanResult(String result) throws InvalidParameterException{
+ List resultNumbers = parseNumbers(result);
+ for (String i : resultNumbers) {
+ Log.i(ChecksRoller.LOG_TAG, i);
+ }
+ if (resultNumbers.size() < 5) {
+ throw new InvalidParameterException();
+ }
+ fn = resultNumbers.get(2);
+ fd = resultNumbers.get(3);
+ fp = resultNumbers.get(4);
+ date = resultNumbers.get(0);
+ sum = resultNumbers.get(1);
+ }
+
+ public ScanResult(String fn, String fd, String fp, String date, String sum) {
+ this.fn = fn;
+ this.fd = fd;
+ this.fp = fp;
+ this.date = date;
+ this.sum = sum;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/Check.java b/Spender/app/src/main/java/com/project/spender/data/entities/Check.java
new file mode 100644
index 0000000..96e0133
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/Check.java
@@ -0,0 +1,119 @@
+package com.project.spender.data.entities;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.PrimaryKey;
+
+import com.project.spender.fns.api.data.Json.CheckJson;
+
+import java.util.Objects;
+
+/**
+ * Класс описывающий чек и его представление в бд.
+ */
+@Entity
+public class Check {
+
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+
+ private String name;
+
+ @ColumnInfo(name = "total_sum")
+ private long totalSum;
+
+ private String shop;
+
+ private String date;
+
+ @Ignore
+ public Check(String name, long totalSum, String shop, String date) {
+ this(0, name, totalSum, shop, date);
+ }
+
+ /**
+ * @param id уникальное число в базе данных, если 0, то генерится автоматически
+ * @param name имя чека
+ * @param totalSum сумма в копейках
+ * @param shop информация о магазине
+ * @param date формат вида yyyy-MM-ddTHH:mm:ss например 2018-05-17T17:57:00
+ */
+ public Check(long id, String name, long totalSum, String shop, String date) {
+ this.id = id;
+ this.name = name;
+ this.totalSum = totalSum;
+ this.shop = shop;
+ this.date = date;
+ }
+
+ @Ignore
+ public Check(CheckJson checkJson) {
+ this(checkJson.getData().user, checkJson.getData().totalSum, checkJson.getData().retailPlaceAddress, checkJson.getData().dateTime);
+ if (name == null) {
+ name = "mini-market Dawn";
+ }
+
+ if (shop == null) {
+ shop = "st. Pushkina-Kukushkina";
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Check check = (Check) o;
+ return totalSum == check.totalSum &&
+ Objects.equals(name, check.name) &&
+ Objects.equals(shop, check.shop) &&
+ Objects.equals(date, check.date);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, totalSum, shop, date);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public long getTotalSum() {
+ return totalSum;
+ }
+
+ public void setTotalSum(long totalSum) {
+ this.totalSum = totalSum;
+ }
+
+ public String getShop() {
+ return shop;
+ }
+
+ public void setShop(String shop) {
+ this.shop = shop;
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/CheckWithProducts.java b/Spender/app/src/main/java/com/project/spender/data/entities/CheckWithProducts.java
new file mode 100644
index 0000000..131b620
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/CheckWithProducts.java
@@ -0,0 +1,82 @@
+package com.project.spender.data.entities;
+
+import androidx.room.Embedded;
+import androidx.room.Ignore;
+import androidx.room.Relation;
+
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.data.Json.Item;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Класс описывающий чек c товарами и его взаимоотношение с таблицами в бд.
+ */
+public class CheckWithProducts {
+
+ @Embedded
+ private Check check;
+
+ @Relation(parentColumn = "id", entityColumn = "check_id")
+ private List products;
+
+ public CheckWithProducts(Check check) {
+ this(check, new ArrayList<>());
+ }
+
+ @Ignore
+ public CheckWithProducts(Check check, List products) {
+ this.check = check;
+ this.products = products;
+ }
+
+ public CheckWithProducts(CheckJson checkJson) {
+ check = new Check(checkJson);
+ products = new ArrayList<>();
+ for (Item item : checkJson.getData().items) {
+ products.add(new Product(item));
+ }
+ }
+
+ public Check getCheck() {
+ return check;
+ }
+
+ public void setCheck(Check check) {
+ this.check = check;
+ }
+
+ public List getProducts() {
+ return products;
+ }
+
+ public void setProducts(List products) {
+ this.products = products;
+ }
+
+ /**
+ * Sets new id for check and new check id for its products
+ */
+ public void updateCheckId (long newId) {
+ check.setId(newId);
+ for (Product product : products) {
+ product.setCheckId(newId);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CheckWithProducts that = (CheckWithProducts) o;
+ return Objects.equals(check, that.check) &&
+ Objects.equals(products, that.products);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(check, products);
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/Product.java b/Spender/app/src/main/java/com/project/spender/data/entities/Product.java
new file mode 100644
index 0000000..c2bad14
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/Product.java
@@ -0,0 +1,156 @@
+package com.project.spender.data.entities;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.project.spender.fns.api.data.Json.Item;
+
+import java.util.Objects;
+
+import static androidx.room.ForeignKey.CASCADE;
+
+
+/**
+ * Класс описывающий товар и его представление в бд.
+ */
+@Entity(foreignKeys =
+ @ForeignKey(onDelete = CASCADE, entity = Check.class, parentColumns = "id", childColumns = "check_id"),
+ indices = {@Index("check_id")})
+public class Product implements Parcelable {
+
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+
+ private String name;
+
+ private long sum;
+
+ private long price;
+
+ private double quantity;
+
+ @ColumnInfo(name = "check_id")
+ private long checkId;
+
+ @Ignore
+ public Product(String name, long sum, long price, double quantity, long checkId) {
+ this(0, name, sum, price, quantity, checkId);
+ }
+
+ public Product(long id, String name, long sum, long price, double quantity, long checkId) {
+ this.id = id;
+ this.name = name;
+ this.sum = sum;
+ this.price = price;
+ this.quantity = quantity;
+ this.checkId = checkId;
+ }
+
+ public Product(Item item) {
+ this(item.name, item.sum, item.price, item.quantity, 0);
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Product product = (Product) o;
+ return sum == product.sum &&
+ price == product.price &&
+ Double.compare(product.quantity, quantity) == 0 &&
+ Objects.equals(name, product.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, sum, price, quantity);
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public long getSum() {
+ return sum;
+ }
+
+ public void setSum(long sum) {
+ this.sum = sum;
+ }
+
+ public long getPrice() {
+ return price;
+ }
+
+ public void setPrice(long price) {
+ this.price = price;
+ }
+
+ public double getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(double quantity) {
+ this.quantity = quantity;
+ }
+
+ public long getCheckId() {
+ return checkId;
+ }
+
+ public void setCheckId(long checkId) {
+ this.checkId = checkId;
+ }
+
+ @Override
+ public String toString() {
+ return name + " " + sum + " " + price + " " + quantity + " " + checkId;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLongArray(new long[]{id, sum, price, checkId});
+ dest.writeString(name);
+ dest.writeDouble(quantity);
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+
+ @Override
+ public Product createFromParcel(Parcel source) {
+ long[] longs = new long[4];
+ source.readLongArray(longs);
+ return new Product(longs[0], source.readString(),
+ longs[1], longs[2], source.readDouble(), longs[3]);
+ }
+
+ @Override
+ public Product[] newArray(int size) {
+ return new Product[size];
+ }
+ };
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/ProductTagJoin.java b/Spender/app/src/main/java/com/project/spender/data/entities/ProductTagJoin.java
new file mode 100644
index 0000000..3e1bb39
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/ProductTagJoin.java
@@ -0,0 +1,46 @@
+package com.project.spender.data.entities;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+
+import static androidx.room.ForeignKey.CASCADE;
+
+/**
+ * Таблица развязки.
+ */
+@Entity(tableName = "product_tag_join",
+ primaryKeys = {"product_id", "tag_id"},
+ foreignKeys = {
+ @ForeignKey(onDelete = CASCADE,
+ entity = Product.class,
+ parentColumns = "id",
+ childColumns = "product_id"),
+ @ForeignKey(onDelete = CASCADE,
+ entity = Tag.class,
+ parentColumns = "id",
+ childColumns = "tag_id")
+ },
+ indices = {@Index("product_id"), @Index("tag_id")}
+ )
+public class ProductTagJoin {
+ @ColumnInfo(name = "product_id")
+ private long productId;
+
+ @ColumnInfo(name = "tag_id")
+ private long tagId;
+
+ public ProductTagJoin(long productId, long tagId) {
+ this.productId = productId;
+ this.tagId = tagId;
+ }
+
+ public long getProductId() {
+ return productId;
+ }
+
+ public long getTagId() {
+ return tagId;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/ProductWithTags.java b/Spender/app/src/main/java/com/project/spender/data/entities/ProductWithTags.java
new file mode 100644
index 0000000..2383200
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/ProductWithTags.java
@@ -0,0 +1,33 @@
+package com.project.spender.data.entities;
+
+import java.util.List;
+
+/**
+ * Structure for product with its tags list
+ */
+public class ProductWithTags {
+
+ private Product product;
+ private List tags;
+
+ public ProductWithTags(Product product, List tags) {
+ this.product = product;
+ this.tags = tags;
+ }
+
+ public Product getProduct() {
+ return product;
+ }
+
+ public void setProduct(Product product) {
+ this.product = product;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public void setTags(List tags) {
+ this.tags = tags;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/Tag.java b/Spender/app/src/main/java/com/project/spender/data/entities/Tag.java
new file mode 100644
index 0000000..1fab49d
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/Tag.java
@@ -0,0 +1,92 @@
+package com.project.spender.data.entities;
+
+import androidx.annotation.Nullable;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import java.util.Objects;
+
+/**
+ * Таблица тегов
+ */
+@Entity(indices = {@Index(value = "name", unique = true ), @Index(value = "color"), @Index(value = "substring")})
+public class Tag {
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+
+ private String name;
+ private int color;
+ @Nullable private String substring;
+
+ @Ignore
+ public Tag(String name) {
+ this(0, name);
+ }
+
+ @Ignore
+ public Tag(long id, String name) {
+ this(id, name, 0);
+ }
+
+
+ public Tag(long id, String name, int color, String substring) {
+ this.id = id;
+ this.name = name;
+ this.color = color;
+ this.substring = substring;
+ }
+
+ @Ignore
+ public Tag(long id, String name, int color) {
+ this(id, name, color, "");
+ }
+
+ @Ignore
+ public Tag(String newTagName, int newTagColor) {
+ this(0, newTagName, newTagColor);
+ }
+
+ @Ignore
+ public Tag(String newTagName, int newTagColor, String substring) {
+ this(0, newTagName, newTagColor, substring);
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public String getSubstring() {
+ return substring;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Tag tag = (Tag) o;
+ return Objects.equals(name, tag.name) && Objects.equals(color, tag.color);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, color);
+ }
+
+ @Override
+ public String toString() {
+ return "Tag{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/TagWithSum.java b/Spender/app/src/main/java/com/project/spender/data/entities/TagWithSum.java
new file mode 100644
index 0000000..4ff5f11
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/TagWithSum.java
@@ -0,0 +1,9 @@
+package com.project.spender.data.entities;
+
+import androidx.room.Embedded;
+
+public class TagWithSum {
+ @Embedded
+ public Tag tag;
+ public long sum;
+}
diff --git a/Spender/app/src/main/java/com/project/spender/data/entities/TagWithSumAndDate.java b/Spender/app/src/main/java/com/project/spender/data/entities/TagWithSumAndDate.java
new file mode 100644
index 0000000..eaadd97
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/data/entities/TagWithSumAndDate.java
@@ -0,0 +1,16 @@
+package com.project.spender.data.entities;
+
+import androidx.room.Embedded;
+
+import java.util.List;
+
+public class TagWithSumAndDate {
+ @Embedded
+ public Tag tag;
+ public long sum;
+ public String date;
+
+ public TagWithSumAndDate() {
+ tag = new Tag(0, "");
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/FnsApi.java b/Spender/app/src/main/java/com/project/spender/fns/api/FnsApi.java
new file mode 100644
index 0000000..4005d50
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/FnsApi.java
@@ -0,0 +1,50 @@
+package com.project.spender.fns.api;
+
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.data.NewUser;
+import com.project.spender.fns.api.data.Phone;
+
+import okhttp3.ResponseBody;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.Field;
+import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+/**
+ *
+ */
+public interface FnsApi {
+// fn -- Номер ФН (Фискальный Номер) — 16-значный номер. Например 8710000100518392
+// operations -- Вид кассового чека. В чеке помечается как n=1 (приход) и n=2 (возврат прихода) (всегда 1)
+// fd -- Номер ФД (Фискальный документ) — до 10 знаков. Например 54812
+// fiscalSign -- Номер ФПД (Фискальный Признак Документа, также известный как ФП) — до 10 знаков. Например 3522207165
+// date -- Дата — дата с чека. Формат может отличаться. Я (автор статьи) пробовал переворачивать дату (т.е. 17-05-2018), ставить вместо Т пробел, удалять секунды
+// sum -- Сумма — сумма с чека в копейках
+
+ @GET("/v1/ofds/*/inns/*/fss/{fnNum}/operations/1/tickets/{fdNum}?")
+ Call isCheckExist(@Path("fnNum") String fn, @Path("fdNum") String fd,
+ @Query("fiscalSign") String fiscalSign, @Query("date") String date,
+ @Query("sum") String sum);
+
+ @GET("/v1/inns/*/kkts/*/fss/{fnNum}/tickets/{fdNum}?")
+ Call getCheck(@Header("Authorization") String loginPassword, @Header("Device-Id") String deviceId, @Header("Device-OS") String deviceOs, @Path("fnNum") String fn, @Path("fdNum") String fd,
+ @Query("fiscalSign") String fiscalSign, @Query("sendToEmail") String yesOrNo);
+
+ @GET("/v1/mobile/users/login")
+ Call login(@Header("Authorization") String loginPassword);
+
+ @POST("/v1/mobile/users/signup")
+ Call signup(@Body NewUser newUser);
+
+ @POST("/v1/mobile/users/restore")
+ Call restore(@Body Phone phone);
+
+ @GET("/v1/inns/*/kkts/*/fss/{fnNum}/tickets/{fdNum}?")
+ Call getRawCheck(@Header("Authorization") String loginPassword, @Header("Device-Id") String deviceId, @Header("Device-OS") String deviceOs, @Path("fnNum") String fn, @Path("fdNum") String fd,
+ @Query("fiscalSign") String fiscalSign, @Query("sendToEmail") String yesOrNo);
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/NetworkManager.java b/Spender/app/src/main/java/com/project/spender/fns/api/NetworkManager.java
new file mode 100644
index 0000000..706782f
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/NetworkManager.java
@@ -0,0 +1,343 @@
+package com.project.spender.fns.api;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.project.spender.data.ScanResult;
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.data.CheckJsonWithStatus;
+import com.project.spender.fns.api.data.NewUser;
+import com.project.spender.fns.api.data.Phone;
+import com.project.spender.fns.api.data.Status;
+import com.project.spender.fns.api.data.StatusWithResponse;
+import com.project.spender.fns.api.exception.NetworkException;
+
+import java.io.IOException;
+
+import okhttp3.Credentials;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+/**
+ * Класс реализующий общение с сетью. Является синглтоном.
+ *
+ */
+public class NetworkManager {
+
+ private class GetCheckCallBack implements Callback {
+ private final MutableLiveData liveData;
+
+ private GetCheckCallBack(@NonNull MutableLiveData liveData) {
+ this.liveData = liveData;
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.code() == OK) {
+ liveData.postValue(new CheckJsonWithStatus(response.body(),
+ Status.SUCCESS, null));
+ } else {
+ liveData.postValue(new CheckJsonWithStatus(
+ null, Status.WRONG_RESPONSE_ERROR,
+ new NetworkException("Check exists, but getCheck return code "
+ + response.code() + " " + response.message(), response)));
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ liveData.postValue(new CheckJsonWithStatus(
+ null, Status.NETWORK_ERROR,
+ new NetworkException(t)));
+ }
+ }
+
+ private final FnsApi fns;
+ public static final String DEFAULT_LOGIN = "+79112813247";
+ public static final String DEFAULT_PASSWORD = "583066";
+
+ public final static int OK_WITHOUT_CONTENT = 204;
+ public final static int OK = 200;
+ public final static int CHECK_NOT_FOUND = 406;
+ public final static int USER_ALREADY_EXISTS = 409;
+ public final static int UNCORRECTED_EMAIL = 400;
+ public final static int UNCORRECTED_PHONE = 500;
+ public final static int UNKNOWN_PHONE = 404;
+ public final static int UNCORRECTED_PHONE_OR_PASSWORD = 403;
+
+ public NetworkManager() {
+ Retrofit retrofit = new Retrofit.Builder()
+ .baseUrl("https://proverkacheka.nalog.ru:9999") //Базовая часть адреса
+ .addConverterFactory(GsonConverterFactory.create()).build(); //Конвертер, необходимый для преобразования JSON'а в объекты
+ fns = retrofit.create(FnsApi.class);
+ }
+
+ /**
+ * Проверяет существование чека через ФНС. Является синхронизованным, поэтому по умолчанию нельзя запускать из main потока.
+ *
+ * @param fn Номер ФН (Фискальный Номер) — 16-значный номер. Например 8710000100518392.
+ * @param fd Номер ФД (Фискальный документ) — до 10 знаков. Например 54812.
+ * @param fiscalSign Номер ФПД (Фискальный Признак Документа, также известный как ФП) — до 10 знаков. Например 3522207165.
+ * @param date Дата — дата с чека. Формат может отличаться.
+ * @param sum Сумма — сумма с чека в копейках.
+ *
+ * @return Код ответа.
+ * 204 -- OK (OK_WITHOUT_CONTENT),
+ * 406 -- не нашел (CHECK_NOT_FOUND),
+ * остальное хз.
+ *
+ * @throws IOException кидается при проблемах соединения с сервером.
+ */
+ public int isCheckExistCodeSync(String fn, String fd, String fiscalSign, String date, String sum)
+ throws IOException {
+
+ return fns.isCheckExist(fn, fd, fiscalSign, date, sum).execute().code();
+ }
+
+ public int isCheckExistCodeSync(ScanResult scanResult)
+ throws IOException {
+
+ return fns.isCheckExist(scanResult.getFn(), scanResult.getFd(), scanResult.getFp(),
+ scanResult.getDate(), scanResult.getSum()).execute().code();
+ }
+
+ @Deprecated
+ public CheckJson getCheckSync(String fn, String fd, String fiscalSign, String date, String sum)
+ throws IOException, NetworkException {
+ return getCheckSync(DEFAULT_LOGIN, DEFAULT_PASSWORD, fn, fd, fiscalSign, date, sum);
+ }
+
+ /**
+ * Получает чек из ФНС и возвращает в виде объекта CheckJson.
+ * Является синхронизованным, поэтому по умолчанию нельзя запускать из main потока.
+ * Внутри себя делает isCheckExistCodeSync.
+ *
+ * @param phone телефон пользователя. Формат : +79991234567
+ * @param password пароль пользователя.
+ * @param fn Номер ФН (Фискальный Номер) — 16-значный номер. Например 8710000100518392.
+ * @param fd Номер ФД (Фискальный документ) — до 10 знаков. Например 54812.
+ * @param fiscalSign Номер ФПД (Фискальный Признак Документа, также известный как ФП) — до 10 знаков. Например 3522207165.
+ * @param date Дата — дата с чека. Формат может отличаться.
+ * @param sum Сумма — сумма с чека в копейках.
+ *
+ * @return чек в виде CheckJson.
+ *
+ * 406 -- не нашел чек (CHECK_NOT_FOUND), т.е либо новые данные еще не дошли до фнс, либо старые уже удалены,
+ * остальное хз.
+ *
+ * @throws IOException кидается при проблемах соединения с сервером.
+ * @throws NetworkException кидается, если ответа не ОК (код можно получить .getCode()).
+ */
+ public CheckJson getCheckSync(String phone, String password, String fn, String fd, String fiscalSign, String date, String sum)
+ throws IOException, NetworkException {
+
+ String loginPassword = Credentials.basic(phone, password);
+ Response responseExist = fns.isCheckExist(fn, fd, fiscalSign, date, sum).execute();
+ if (responseExist.code() != 204) {
+ throw new NetworkException("Check doesnt exit, returns " + responseExist.code(), responseExist);
+ }
+
+ Response res = fns.getCheck(loginPassword, "", "",
+ fn, fd, fiscalSign, "no").execute();
+
+ if (res.code() != 200) {
+ throw new NetworkException("Check exists, but getCheck return code " + res.code() + " " + res.message() , res);
+ }
+ return res.body();
+ }
+
+ @Deprecated
+ public CheckJson getCheckSync(ScanResult scanResult)
+ throws IOException, NetworkException {
+ return getCheckSync(DEFAULT_LOGIN, DEFAULT_PASSWORD, scanResult);
+ }
+
+ public CheckJson getCheckSync(String phone, String password, ScanResult scanResult)
+ throws IOException, NetworkException {
+
+ return getCheckSync(phone, password, scanResult.getFn(), scanResult.getFd(), scanResult.getFp(),
+ scanResult.getDate(), scanResult.getSum());
+ }
+
+ /**
+ * Венец творения.
+ * Асинхронное получение чека. Внутри себя делает асинхронную проверку существования чека.
+ * Следить за ходом вычисления можно с помощью LiveData (тупой пример в androidTest/...).
+ *
+ * @param phone телефон пользователя. Формат : +79991234567
+ * @param password пароль пользователя.
+ * @param fn Номер ФН (Фискальный Номер) — 16-значный номер. Например 8710000100518392.
+ * @param fd Номер ФД (Фискальный документ) — до 10 знаков. Например 54812.
+ * @param fiscalSign Номер ФПД (Фискальный Признак Документа, также известный как ФП) — до 10 знаков. Например 3522207165.
+ * @param date Дата — дата с чека. Формат может отличаться.
+ * @param sum Сумма — сумма с чека в копейках.
+ *
+ * @return Объект типа LiveData. Позволяет узнать текущее состояние получения.
+ * Есть возможность подписки. Также хранит исключение прервавшее работу.
+ */
+ private LiveData getCheckAsync(final String phone, final String password, final String fn, final String fd,
+ final String fiscalSign, String date, String sum) {
+
+ MutableLiveData liveData = new MutableLiveData<>();
+ liveData.postValue(new CheckJsonWithStatus(null, Status.SENDING, null));
+
+ String loginPassword = Credentials.basic(phone, password);
+ /*
+ Moved only one to a new class, since the class for checking the existence
+ would have too many constructor parameters in my opinion
+ */
+ fns.isCheckExist(fn, fd, fiscalSign, date, sum).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.code() == OK_WITHOUT_CONTENT) {
+ liveData.postValue(
+ new CheckJsonWithStatus(null, Status.EXIST, null));
+
+ fns.getCheck(loginPassword, "", "",
+ fn, fd, fiscalSign, "no").enqueue(new GetCheckCallBack(liveData));
+ } else {
+ liveData.postValue(new CheckJsonWithStatus(
+ null, Status.WRONG_RESPONSE_ERROR,
+ new NetworkException("Check doesn't exist. Response: "
+ + response.code() + " " + response.message() , response)));
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ liveData.postValue(new CheckJsonWithStatus(
+ null, Status.NETWORK_ERROR,
+ new NetworkException(t)));
+ }
+ });
+ return liveData;
+ }
+
+ @Deprecated
+ public LiveData getCheckAsync(ScanResult scanResult) {
+ return getCheckAsync(DEFAULT_LOGIN, DEFAULT_PASSWORD, scanResult);
+ }
+
+ public LiveData getCheckAsync(String phone, String password, ScanResult scanResult) {
+ return getCheckAsync(phone, password, scanResult.getFn(), scanResult.getFd(), scanResult.getFp(),
+ scanResult.getDate(), scanResult.getSum());
+ }
+
+ /**
+ * Регистрация нового пользователя. Пароль придет в виде смс на указанный номер.
+ *
+ * @param newUser обьект со всеми необходимыми данными.
+ * @return код ответа:
+ * 204 -- OK (OK_WITHOUT_CONTENT),
+ * 409 -- пользователь уже существует (USER_ALREADY_EXISTS),
+ * 500 -- номер телефона некорректный (UNCORRECTED_PHONE),
+ * 400 -- адрес электронной почты некорректный (UNCORRECTED_EMAIL).
+ * остальное хз.
+ * @throws IOException кидается при проблемах соединения с сервером.
+ */
+ public int registrationSync(NewUser newUser) throws IOException {
+ return fns.signup(newUser).execute().code();
+ }
+
+ public LiveData registrationAsync(NewUser newUser) {
+ MutableLiveData liveData = new MutableLiveData<>();
+ liveData.postValue(new StatusWithResponse(Status.SENDING, null));
+ fns.signup(newUser).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.code() == OK_WITHOUT_CONTENT) {
+ liveData.postValue(new StatusWithResponse(Status.SUCCESS, null));
+ } else {
+ liveData.postValue(new StatusWithResponse(Status.WRONG_RESPONSE_ERROR,
+ new NetworkException("Wrong response: " + response.code() + " " + response.message(), response )));
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ liveData.postValue(new StatusWithResponse(Status.NETWORK_ERROR, new NetworkException(t)));
+ }
+ });
+
+ return liveData;
+ }
+
+ /**
+ * Восстановление пароля. Новый пароль придет в виде смс на указанный номер.
+ *
+ * @param phone номер телефона. Формат : +79991234567
+ * @return код ответа:
+ * 204 -- OK (OK_WITHOUT_CONTENT),
+ * 404 -- номер телефона не найден или номер некорректный (UNKNOWN_PHONE),
+ * остальное хз.
+ * @throws IOException кидается при проблемах соединения с сервером.
+ */
+ public int restorePasswordSync(String phone) throws IOException {
+ return fns.restore(new Phone(phone)).execute().code();
+ }
+
+ public LiveData restorePasswordAsync(String phone) {
+ MutableLiveData liveData = new MutableLiveData<>();
+ liveData.postValue(new StatusWithResponse(Status.SENDING, null));
+ fns.restore(new Phone(phone)).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.code() == OK_WITHOUT_CONTENT) {
+ liveData.postValue(new StatusWithResponse(Status.SUCCESS, null));
+ } else {
+ liveData.postValue(new StatusWithResponse(Status.WRONG_RESPONSE_ERROR,
+ new NetworkException("Wrong response: " + response.code() + " " + response.message(), response )));
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ liveData.postValue(new StatusWithResponse(Status.NETWORK_ERROR, new NetworkException(t)));
+ }
+ });
+
+ return liveData;
+ }
+
+ /**
+ * Проверка корректности телефона и пароля.
+ *
+ * @param phone телефон пользователя. Формат : +79991234567
+ * @param password пароль пользователя.
+ * @return код ответа:
+ * 200 -- ОК,
+ * 403 -- некорректный номер телефона или пароль,
+ * остальное хз.
+ * @throws IOException кидается при проблемах соединения с сервером.
+ */
+ public int checkUserSync(String phone, String password) throws IOException {
+ return fns.login(Credentials.basic(phone, password)).execute().code();
+ }
+
+ public LiveData checkUserAsync(String phone, String password) {
+ MutableLiveData liveData = new MutableLiveData<>();
+ liveData.postValue(new StatusWithResponse(Status.SENDING, null));
+ fns.login(Credentials.basic(phone, password)).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.code() == OK) {
+ liveData.postValue(new StatusWithResponse(Status.SUCCESS, null));
+ } else {
+ liveData.postValue(new StatusWithResponse(Status.WRONG_RESPONSE_ERROR,
+ new NetworkException("Wrong response: " + response.code() + " " + response.message(), response )));
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ liveData.postValue(new StatusWithResponse(Status.NETWORK_ERROR, new NetworkException(t)));
+ }
+ });
+
+ return liveData;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/CheckJsonWithStatus.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/CheckJsonWithStatus.java
new file mode 100644
index 0000000..11b088c
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/CheckJsonWithStatus.java
@@ -0,0 +1,21 @@
+package com.project.spender.fns.api.data;
+
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.exception.NetworkException;
+
+/**
+ * Класс для отображения статуса запроса и возврашения результата или исключения.
+ */
+public class CheckJsonWithStatus extends StatusWithResponse{
+
+ private CheckJson checkJson;
+
+ public CheckJsonWithStatus(CheckJson checkJson, Status status, NetworkException exception) {
+ super(status, exception);
+ this.checkJson = checkJson;
+ }
+
+ public CheckJson getCheckJson() {
+ return checkJson;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/CheckJson.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/CheckJson.java
new file mode 100644
index 0000000..97fee47
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/CheckJson.java
@@ -0,0 +1,16 @@
+package com.project.spender.fns.api.data.Json;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class CheckJson {
+
+ @SerializedName("document")
+ @Expose
+ private Document document;
+
+ public Receipt getData() {
+ return document.receipt;
+ }
+
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Document.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Document.java
new file mode 100644
index 0000000..1ebcd45
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Document.java
@@ -0,0 +1,13 @@
+package com.project.spender.fns.api.data.Json;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class Document {
+
+ @SerializedName("receipt")
+ @Expose
+ public Receipt receipt;
+
+
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Item.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Item.java
new file mode 100644
index 0000000..0e102ec
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Item.java
@@ -0,0 +1,32 @@
+package com.project.spender.fns.api.data.Json;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class Item {
+
+ @SerializedName("sum")
+ @Expose
+ public long sum;
+
+ @SerializedName("price")
+ @Expose
+ public long price;
+
+ @SerializedName("name")
+ @Expose
+ public String name;
+
+ @SerializedName("quantity")
+ @Expose
+ public double quantity;
+
+ @SerializedName("nds10")
+ @Expose
+ public long nds10;
+
+ @Override
+ public String toString() {
+ return name + " " + sum + " " + price + " " + quantity;
+ }
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Property.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Property.java
new file mode 100644
index 0000000..b300554
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Property.java
@@ -0,0 +1,16 @@
+package com.project.spender.fns.api.data.Json;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class Property {
+
+ @SerializedName("value")
+ @Expose
+ public String value;
+
+ @SerializedName("key")
+ @Expose
+ public String key;
+
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Receipt.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Receipt.java
new file mode 100644
index 0000000..4173c61
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Json/Receipt.java
@@ -0,0 +1,94 @@
+package com.project.spender.fns.api.data.Json;
+
+
+import java.util.List;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class Receipt {
+
+ @SerializedName("operationType")
+ @Expose
+ public long operationType;
+
+ @SerializedName("fiscalSign")
+ @Expose
+ public long fiscalSign;
+
+ @SerializedName("dateTime")
+ @Expose
+ public String dateTime;
+
+ @SerializedName("rawData")
+ @Expose
+ public String rawData;
+
+ @SerializedName("totalSum")
+ @Expose
+ public long totalSum;
+
+ @SerializedName("nds10")
+ @Expose
+ public long nds10;
+
+ @SerializedName("userInn")
+ @Expose
+ public String userInn;
+
+ @SerializedName("taxationType")
+ @Expose
+ public long taxationType;
+
+ @SerializedName("operator")
+ @Expose
+ public String operator;
+
+ @SerializedName("fiscalDocumentNumber")
+ @Expose
+ public long fiscalDocumentNumber;
+
+ @SerializedName("properties")
+ @Expose
+ public List properties = null;
+
+ @SerializedName("receiptCode")
+ @Expose
+ public long receiptCode;
+
+ @SerializedName("requestNumber")
+ @Expose
+ public long requestNumber;
+
+ @SerializedName("user")
+ @Expose
+ public String user;
+
+ @SerializedName("kktRegId")
+ @Expose
+ public String kktRegId;
+
+ @SerializedName("fiscalDriveNumber")
+ @Expose
+ public String fiscalDriveNumber;
+
+ @SerializedName("items")
+ @Expose
+ public List- items = null;
+
+ @SerializedName("ecashTotalSum")
+ @Expose
+ public long ecashTotalSum;
+
+ @SerializedName("retailPlaceAddress")
+ @Expose
+ public String retailPlaceAddress;
+
+ @SerializedName("cashTotalSum")
+ @Expose
+ public long cashTotalSum;
+
+ @SerializedName("shiftNumber")
+ @Expose
+ public long shiftNumber;
+
+}
\ No newline at end of file
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/NewUser.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/NewUser.java
new file mode 100644
index 0000000..4c07ff1
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/NewUser.java
@@ -0,0 +1,18 @@
+package com.project.spender.fns.api.data;
+
+/**
+ * Объект для нового пользователя.
+ * email и name используются только при регистрации и больше нигде.
+ * Формат номера: +79991234567
+ */
+public class NewUser {
+ public String email;
+ public String name;
+ public String phone;
+
+ public NewUser(String name, String email, String phone) {
+ this.name = name;
+ this.email = email;
+ this.phone = phone;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Phone.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Phone.java
new file mode 100644
index 0000000..aa48052
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Phone.java
@@ -0,0 +1,12 @@
+package com.project.spender.fns.api.data;
+
+/**
+ * Structure for phone number
+ */
+public class Phone {
+ public String phone;
+
+ public Phone(String phone) {
+ this.phone = phone;
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/Status.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/Status.java
new file mode 100644
index 0000000..5baaaf5
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/Status.java
@@ -0,0 +1,12 @@
+package com.project.spender.fns.api.data;
+
+/**
+ * Status of request to FSN base
+ */
+public enum Status {
+ SUCCESS, //check received
+ EXIST, //check exists, but not received yet
+ WRONG_RESPONSE_ERROR, //unexpected response code
+ NETWORK_ERROR, // cannot connect with server
+ SENDING //request for existing
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/data/StatusWithResponse.java b/Spender/app/src/main/java/com/project/spender/fns/api/data/StatusWithResponse.java
new file mode 100644
index 0000000..6efec47
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/data/StatusWithResponse.java
@@ -0,0 +1,64 @@
+package com.project.spender.fns.api.data;
+
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.exception.NetworkException;
+
+import static com.project.spender.fns.api.NetworkManager.CHECK_NOT_FOUND;
+import static com.project.spender.fns.api.NetworkManager.UNCORRECTED_EMAIL;
+import static com.project.spender.fns.api.NetworkManager.UNCORRECTED_PHONE;
+import static com.project.spender.fns.api.NetworkManager.UNCORRECTED_PHONE_OR_PASSWORD;
+import static com.project.spender.fns.api.NetworkManager.UNKNOWN_PHONE;
+import static com.project.spender.fns.api.NetworkManager.USER_ALREADY_EXISTS;
+
+/**
+ * Status with possible exception
+ */
+public class StatusWithResponse {
+ private Status status;
+ private NetworkException exception;
+
+ public StatusWithResponse(Status status, NetworkException exception) {
+ this.status = status;
+ this.exception = exception;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public NetworkException getException() {
+ return exception;
+ }
+
+ /**
+ * Returns text representation on the status
+ */
+ public String getUserReadableMassage() {
+ switch (status) {
+ case SUCCESS:
+ return "Success";
+ case EXIST:
+ return "Check exists";
+ case SENDING:
+ return "Sending";
+ case NETWORK_ERROR:
+ return "Network error";
+ case WRONG_RESPONSE_ERROR:
+ switch (exception.getCode()) {
+ case CHECK_NOT_FOUND:
+ return "Check not found";
+ case USER_ALREADY_EXISTS:
+ return "User already exists";
+ case UNCORRECTED_EMAIL :
+ return "Incorrect email address";
+ case UNCORRECTED_PHONE:
+ return "Incorrect phone number";
+ case UNKNOWN_PHONE:
+ return "Phone number not found";
+ case UNCORRECTED_PHONE_OR_PASSWORD:
+ return "Incorrect phone number or password";
+ }
+ }
+ return "Unknown error";
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fns/api/exception/NetworkException.java b/Spender/app/src/main/java/com/project/spender/fns/api/exception/NetworkException.java
new file mode 100644
index 0000000..d61bfc6
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fns/api/exception/NetworkException.java
@@ -0,0 +1,28 @@
+package com.project.spender.fns.api.exception;
+
+import retrofit2.Response;
+
+/**
+ * Исключение кидаемое NetworkManager.
+ * Может быть пораждено неправильным http кодом, либо другим исключением.
+ */
+public class NetworkException extends Exception {
+ private Response response;
+
+ public int getCode() {
+ return (response != null)? response.code() : -1;
+ }
+
+ public String cleverGetMessage() {
+ return (response != null)? response.message() : "-1";
+ }
+
+ public NetworkException(String message, Response response) {
+ super(message);
+ this.response = response;
+ }
+
+ public NetworkException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fragments/ChartFragment.java b/Spender/app/src/main/java/com/project/spender/fragments/ChartFragment.java
new file mode 100644
index 0000000..d5d2704
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fragments/ChartFragment.java
@@ -0,0 +1,39 @@
+package com.project.spender.fragments;
+
+import androidx.fragment.app.Fragment;
+
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+import com.project.spender.data.CheckDao;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+
+public abstract class ChartFragment extends Fragment {
+
+ @Inject protected ChecksRoller checksRoller;
+
+ protected String leftDate = "1999-01-25";
+ protected String rightDate = "now";
+ protected Set whiteIdList;
+ protected CheckDao checkDao;
+
+ public ChartFragment() {
+ super();
+ App.getComponent().inject(this);
+ checkDao = checksRoller.getAppDatabase().getCheckDao();
+ }
+
+ public void setWhiteIdList(Set whiteIdList) {
+ this.whiteIdList = whiteIdList;
+ }
+
+ public void setPeriod(String leftDate, String rightDate) {
+ this.leftDate = leftDate;
+ this.rightDate = rightDate;
+ }
+
+ public abstract void resetData();
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fragments/LineChartFragment.java b/Spender/app/src/main/java/com/project/spender/fragments/LineChartFragment.java
new file mode 100644
index 0000000..7698d37
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fragments/LineChartFragment.java
@@ -0,0 +1,59 @@
+package com.project.spender.fragments;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import com.project.spender.R;
+import com.project.spender.charts.LineChartController;
+
+import java.util.Set;
+
+public class LineChartFragment extends ChartFragment {
+
+ private LineChartController lineChartController;
+
+ public LineChartFragment() {
+ // Required empty public constructor
+ }
+
+ public static LineChartFragment newInstance() {
+ return new LineChartFragment();
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_line_chart, container, false);
+
+ lineChartController = new LineChartController(getViewLifecycleOwner(), view.findViewById(R.id.lineChart));
+ lineChartController.setWhiteIdList(whiteIdList);
+ resetData();
+
+ return view;
+ }
+
+ @Override
+ public void setWhiteIdList(Set whiteIdList) {
+ super.setWhiteIdList(whiteIdList);
+ if (lineChartController != null) {
+ lineChartController.setWhiteIdList(whiteIdList);
+ }
+ }
+
+ @Override
+ public void resetData() {
+ if (lineChartController != null) {
+ lineChartController.setDataSource(checkDao.getTagsWithSumAndDateByDate(leftDate, rightDate));
+ }
+ }
+
+ public void invalidate() {
+ if (lineChartController != null) {
+ lineChartController.invalidate();
+ }
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fragments/PieChartFragment.java b/Spender/app/src/main/java/com/project/spender/fragments/PieChartFragment.java
new file mode 100644
index 0000000..2946d3c
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fragments/PieChartFragment.java
@@ -0,0 +1,98 @@
+package com.project.spender.fragments;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+import com.project.spender.charts.PieChartController;
+import com.project.spender.R;
+import com.project.spender.roller.App;
+import com.project.spender.roller.ChecksRoller;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+
+
+public class PieChartFragment extends ChartFragment {
+
+ @Inject protected ChecksRoller checksRoller;
+ private ImageButton secret;
+ private int clickCounter;
+ private final static int MAGIC_CONST = 30;
+
+ private PieChartController pieChartController;
+
+ public PieChartFragment() {
+ // Required empty public constructor
+ }
+
+ public static PieChartFragment newInstance() {
+ return new PieChartFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ App.getComponent().inject(this);
+
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_pie_chart, container, false);
+
+ secret = view.findViewById(R.id.secret);
+ secret.setOnClickListener(v -> {
+ clickCounter++;
+ if (clickCounter > MAGIC_CONST) {
+ secret.setImageResource(R.drawable.clevercat);
+// clickCounter = 0;
+ } else {
+ secret.setImageResource(R.drawable.cat);
+ }
+
+ checksRoller.setCatMode();
+ });
+
+ //Pie
+ pieChartController = new PieChartController(getViewLifecycleOwner(), view.findViewById(R.id.pieChart));
+ pieChartController.setWhiteIdList(whiteIdList);
+ resetData();
+
+ // Inflate the layout for this fragment
+ return view;
+ }
+
+ @Override
+ public void setWhiteIdList(Set whiteIdList) {
+ super.setWhiteIdList(whiteIdList);
+ if (pieChartController != null) {
+ pieChartController.setWhiteIdList(whiteIdList);
+ }
+ }
+
+ public void drawHole(boolean status) {
+ if (status) {
+ secret.setVisibility(View.VISIBLE);
+ } else {
+ secret.setVisibility(View.GONE);
+ }
+ pieChartController.drawHole(status);
+ pieChartController.invalidate();
+ }
+
+ @Override
+ public void resetData() {
+ if (pieChartController != null) {
+ pieChartController.setDataSource(checkDao.getTagsWithSumByData(leftDate, rightDate));
+ pieChartController.animate();
+ }
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/fragments/StackedBarChartFragment.java b/Spender/app/src/main/java/com/project/spender/fragments/StackedBarChartFragment.java
new file mode 100644
index 0000000..3ccbe50
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/fragments/StackedBarChartFragment.java
@@ -0,0 +1,60 @@
+package com.project.spender.fragments;
+
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.github.mikephil.charting.charts.LineChart;
+import com.project.spender.R;
+import com.project.spender.charts.StackedBarChartController;
+
+import java.util.Set;
+
+public class StackedBarChartFragment extends ChartFragment {
+
+ private StackedBarChartController stackedBarChartController;
+
+ public StackedBarChartFragment() {
+ // Required empty public constructor
+ }
+
+ public static StackedBarChartFragment newInstance() {
+ return new StackedBarChartFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_stacked_bar_chart, container, false);
+ stackedBarChartController = new StackedBarChartController(getViewLifecycleOwner(), view.findViewById(R.id.barChart));
+ stackedBarChartController.setWhiteIdList(whiteIdList);
+ resetData();
+ return view;
+ }
+
+ @Override
+ public void resetData() {
+ if (stackedBarChartController != null) {
+ stackedBarChartController.setDataSource(checkDao.getTagsWithSumAndDateByDateOderedByDate(leftDate, rightDate));
+ }
+ }
+
+ @Override
+ public void setWhiteIdList(Set whiteIdList) {
+ super.setWhiteIdList(whiteIdList);
+ if (stackedBarChartController != null) {
+ stackedBarChartController.setWhiteIdList(whiteIdList);
+ }
+ }
+}
diff --git a/Spender/app/src/main/java/com/project/spender/roller/App.java b/Spender/app/src/main/java/com/project/spender/roller/App.java
new file mode 100644
index 0000000..59dac13
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/roller/App.java
@@ -0,0 +1,23 @@
+package com.project.spender.roller;
+
+import android.app.Application;
+
+public class App extends Application {
+
+ private static AppComponent component;
+
+ public static AppComponent getComponent() {
+ return component;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ component = buildComponent();
+ }
+
+ protected AppComponent buildComponent() {
+ return DaggerAppComponent.builder().appModule(new AppModule(this)).checksRollerModule(new ChecksRollerModule()).build();
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/roller/AppComponent.java b/Spender/app/src/main/java/com/project/spender/roller/AppComponent.java
new file mode 100644
index 0000000..df0f32b
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/roller/AppComponent.java
@@ -0,0 +1,37 @@
+package com.project.spender.roller;
+
+import com.project.spender.activities.HistoryActivity;
+import com.project.spender.activities.LoginActivity;
+import com.project.spender.activities.MainActivity;
+import com.project.spender.activities.ScanActivity;
+import com.project.spender.adapters.ItemAdapter;
+import com.project.spender.adapters.ListAdapter;
+import com.project.spender.controllers.CheckListHolder;
+import com.project.spender.controllers.CheckShowHelper;
+import com.project.spender.controllers.TagChoiceHelper;
+import com.project.spender.controllers.TagListHelper;
+import com.project.spender.controllers.TagStateHolder;
+import com.project.spender.fragments.ChartFragment;
+import com.project.spender.fragments.PieChartFragment;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+@Component(modules = {ChecksRollerModule.class, AppModule.class})
+@Singleton
+public interface AppComponent {
+ void inject(HistoryActivity activity);
+ void inject(LoginActivity activity);
+ void inject(ChartFragment fragment);
+ void inject(PieChartFragment fragment);
+ void inject(CheckShowHelper helper);
+ void inject(CheckListHolder holder);
+ void inject(TagListHelper helper);
+ void inject(TagChoiceHelper helper);
+ void inject(MainActivity activity);
+ void inject(ScanActivity activity);
+ void inject(ItemAdapter adapter);
+ void inject(TagStateHolder holder);
+ void inject(ListAdapter adapter);
+}
diff --git a/Spender/app/src/main/java/com/project/spender/roller/AppModule.java b/Spender/app/src/main/java/com/project/spender/roller/AppModule.java
new file mode 100644
index 0000000..c1082ed
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/roller/AppModule.java
@@ -0,0 +1,27 @@
+package com.project.spender.roller;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class AppModule {
+
+ private @NonNull Context context;
+
+ public AppModule(@NonNull Context context) {
+ this.context = context;
+ }
+
+ @Provides
+ @Singleton
+ public Context provideContext() {
+ return context;
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/roller/ChecksRoller.java b/Spender/app/src/main/java/com/project/spender/roller/ChecksRoller.java
new file mode 100644
index 0000000..1afd8b5
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/roller/ChecksRoller.java
@@ -0,0 +1,303 @@
+package com.project.spender.roller;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.room.Room;
+
+import com.project.spender.activities.LoginActivity;
+import com.project.spender.controllers.HistoryListHolder;
+import com.project.spender.data.AppDatabase;
+import com.project.spender.data.CheckStatus;
+import com.project.spender.data.ScanResult;
+import com.project.spender.data.entities.Check;
+import com.project.spender.data.entities.CheckWithProducts;
+import com.project.spender.data.entities.Product;
+import com.project.spender.data.entities.ProductTagJoin;
+import com.project.spender.data.entities.Tag;
+import com.project.spender.fns.api.NetworkManager;
+import com.project.spender.fns.api.data.CheckJsonWithStatus;
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.data.NewUser;
+import com.project.spender.fns.api.data.Status;
+import com.project.spender.fns.api.data.StatusWithResponse;
+import com.project.spender.fns.api.exception.NetworkException;
+
+import java.io.EOFException;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+/**
+ * Singleton class to control interaction of ui and ui helpers with database and network
+ */
+public class ChecksRoller {
+
+ private NetworkManager networkManager;
+ private AppDatabase appDatabase;
+ private LifecycleOwner owner;
+
+ public static final String LOG_TAG = "KITPRIVIT";
+ private static final String DATABASE = "DataBase";
+ private static final String ACCOUNT_INFO = "settings";
+ private static final String ACCOUNT_LOGIN = "login";
+ private static final String ACCOUNT_PASSWORD = "password";
+ private static SharedPreferences accountInfo;
+ private boolean superCatMode = false;
+
+ private HistoryListHolder historyListHolder = new HistoryListHolder();
+ private ExecutorService executor;
+
+ private String number;
+ private String password;
+
+ /**
+ * Set super user mod with awesome car image and ability to use Misha's login and password
+ */
+ public void setCatMode() {
+ superCatMode = true;
+ }
+
+ /**
+ * Tells if cat mode on
+ */
+ public boolean getCatMode() {
+ return superCatMode;
+ }
+
+ /**
+ * Returns class that contains check requests history
+ */
+ public HistoryListHolder getHistoryListHolder() {
+ return historyListHolder;
+ }
+
+ /**
+ * Saves new number and password to local settings.
+ * If parameter did not change put null to it
+ */
+ public void saveAccountInfo(@Nullable String name, @Nullable String password) {
+ SharedPreferences.Editor editor = accountInfo.edit();
+ if (name != null) {
+ editor.putString(ACCOUNT_LOGIN, name);
+ }
+ if (password != null) {
+ editor.putString(ACCOUNT_PASSWORD, password);
+ }
+ editor.apply();
+ }
+
+ /**
+ * Removes number and password from local settings
+ */
+ public void clearAccountInfo() {
+ SharedPreferences.Editor editor = accountInfo.edit();
+ editor.clear();
+ editor.apply();
+ }
+
+ /**
+ * Updates local variables in accordance with saved settings
+ */
+ private void updateLoginInfo() {
+ number = accountInfo.getString(ACCOUNT_LOGIN, null);
+ password = accountInfo.getString(ACCOUNT_PASSWORD, null);
+ }
+
+ /**
+ * Returns class for interaction with database
+ */
+ public AppDatabase getAppDatabase() {
+ return appDatabase;
+ }
+
+ /**
+ * Initializes all private fields
+ * @param context owner of database and network controllers
+ */
+ public ChecksRoller(Context context) {
+ executor = Executors.newSingleThreadExecutor();
+ accountInfo = context.getSharedPreferences(ACCOUNT_INFO, Context.MODE_PRIVATE);
+ networkManager = new NetworkManager();
+ appDatabase = Room.databaseBuilder(context, AppDatabase.class, DATABASE).fallbackToDestructiveMigration().build();
+ updateLoginInfo();
+ if (number == null || password == null) {
+ Intent intent = new Intent(context, LoginActivity.class);
+ Toast.makeText(context,
+ "Authorization required to receive checks", Toast.LENGTH_LONG).show();
+ context.startActivity(intent);
+ }
+ }
+
+ /**
+ * Sets new lifecycle owner for tag adding
+ */
+ public void setOwner(LifecycleOwner owner) {
+ this.owner = owner;
+ }
+
+ /**
+ * Inserts existing tag for given product
+ */
+ public void insertTagForProductById(long tagId, long productId) {
+ executor.submit(() -> appDatabase.getCheckDao().insertExistingTagForProduct(tagId, productId));
+ }
+
+ /**
+ * Removes tag for the product
+ */
+ public void deleteTagForProduct(long tagId, long productId) {
+ executor.submit(() -> appDatabase.getCheckDao()
+ .deleteTagProductRelation(new ProductTagJoin(productId, tagId)));
+ }
+
+ /**
+ * Removes product by id
+ */
+ public void deleteProduct(long productId) {
+ executor.submit(() -> appDatabase.getCheckDao().deleteProductById(productId));
+ }
+
+ /**
+ * Removes check by id
+ */
+ public void deleteCheck(long checkId) {
+ executor.submit(() -> appDatabase.getCheckDao().deleteCheckById(checkId));
+ }
+ /**
+ * Removes tag by id
+ */
+ public void deleteTag(long tagId) {
+ executor.submit(() -> appDatabase.getCheckDao().deleteTagById(tagId));
+ }
+
+ /**
+ * Removes check by id
+ */
+ public void addTag(String name, String regEx, int color) {
+ executor.submit(() -> getAppDatabase().getCheckDao().insertTag(new Tag(name, color, regEx)));
+ }
+
+ /**
+ * Adds check with the most delicious cheese to check list
+ */
+ public void cheese() {
+ Check check = new Check(0, "Typical",300000, "Auchan", "2007-05-18T22:05:00");
+ Product product1 = new Product("Lambert", 100000, 150000, 100, 0);
+ Product product2 = new Product("Oltermanni", 100000, 100000, 100, 0);
+ Product product3 = new Product("Larec", 100000, 70000, 100, 0);
+
+ executor.submit(() -> appDatabase.getCheckDao().insertCheckWithProducts(new CheckWithProducts(check,
+ Arrays.asList(product1, product2, product3))));
+
+ }
+
+ /**
+ * Sends restore password request
+ * @return livedata with request status
+ */
+ public LiveData restore(String number) {
+ return networkManager.restorePasswordAsync(number);
+ }
+
+ /**
+ * Sends registration request
+ * @return livedata with request status
+ */
+ public LiveData registration(String name, String email, String number) {
+ NewUser user = new NewUser(name, email, number);
+ return networkManager.registrationAsync(user);
+ }
+
+ private void addTagsIfMatch(Product product, List tags) {
+ for (Tag j : tags) {
+ if (j.getSubstring() != null && product.getName().matches(".*" + j.getSubstring() + ".*")) {
+ executor.submit(() -> appDatabase.getCheckDao().insertTagForProduct(j, product.getId()));
+ }
+ }
+ }
+
+ private synchronized void putCheck(CheckJson check) {
+ CheckWithProducts newCheck = new CheckWithProducts(check);
+ executor.submit(() -> appDatabase.getCheckDao().insertCheckWithProducts(newCheck));
+ for (Product i : newCheck.getProducts()) {
+ LiveData
> tags = appDatabase.getCheckDao().getAllTags();
+ tags.observe(owner, tags1 -> addTagsIfMatch(i, tags1));
+ }
+ }
+
+ /**
+ * Requests for check with parameters
+ * @return if parameters correct (0) or not (-1)
+ */
+ public synchronized int requestCheck(ScanResult result){
+ updateLoginInfo();
+ Log.i(ChecksRoller.LOG_TAG, number + " " + password);
+ if (number == null || password == null) {
+ Log.i(LOG_TAG, "Missing user info to get checks");
+ return -1;
+ }
+
+ CheckStatus status = new CheckStatus(Calendar.getInstance().getTime().toString());
+ historyListHolder.add(status);
+ tryCheck(result, status);
+
+ return 0;
+ }
+
+ private synchronized void tryCheck(ScanResult result, CheckStatus status) {
+
+ LiveData liveData = networkManager.getCheckAsync(number, password, result);
+
+ liveData.observeForever(checkJsonWithStatus -> {
+ historyListHolder.update();
+ if (checkJsonWithStatus == null) {
+ status.settStatus("Not received yet");
+ return;
+ }
+
+ status.settStatus(checkJsonWithStatus.getUserReadableMassage());
+
+ if (checkJsonWithStatus.getStatus() == Status.WRONG_RESPONSE_ERROR) {
+ NetworkException e = checkJsonWithStatus.getException();
+ Log.i(ChecksRoller.LOG_TAG, "Error while loading check " + e + " | "
+ + e.getMessage() + " | " + e.getCode() + " | "+ e.getCause());
+ if (e.getCause() != null && EOFException.class.isAssignableFrom(e.getCause().getClass()) && status.getCounter() < 3) {
+ status.incCounter();
+ tryCheck(result, status);
+ }
+ return;
+ }
+
+ if (checkJsonWithStatus.getStatus() == Status.SUCCESS) {
+ Log.i(ChecksRoller.LOG_TAG, "Check received");
+ putCheck(checkJsonWithStatus.getCheckJson());
+ }
+ });
+ }
+
+ /**
+ * Returns all checks in given interval matches given regular expression
+ */
+ public LiveData> findChecksByTimePeriodAndRegEx(String begin, String end, String regEx) {
+ return appDatabase.getCheckDao().getChecksWithProductsByDateAndRegEx(regEx, begin, end);
+ }
+
+ /**
+ * Removes all checks from database
+ */
+ public void onRemoveAllClicked() {
+ executor.submit(() -> appDatabase.getCheckDao().deleteAll());
+ Log.i(LOG_TAG, "All items removed");
+ }
+
+}
diff --git a/Spender/app/src/main/java/com/project/spender/roller/ChecksRollerModule.java b/Spender/app/src/main/java/com/project/spender/roller/ChecksRollerModule.java
new file mode 100644
index 0000000..e3bc576
--- /dev/null
+++ b/Spender/app/src/main/java/com/project/spender/roller/ChecksRollerModule.java
@@ -0,0 +1,22 @@
+package com.project.spender.roller;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class ChecksRollerModule {
+
+ @Provides
+ @Singleton
+ @NonNull
+ public ChecksRoller provideChecksRoller(Context context) {
+ return new ChecksRoller(context);
+ }
+
+}
diff --git a/Spender/app/src/main/music/PoliceSound.mp3 b/Spender/app/src/main/music/PoliceSound.mp3
new file mode 100644
index 0000000..b3131e5
Binary files /dev/null and b/Spender/app/src/main/music/PoliceSound.mp3 differ
diff --git a/Spender/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Spender/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1f6bb29
--- /dev/null
+++ b/Spender/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Spender/app/src/main/res/drawable/browser.png b/Spender/app/src/main/res/drawable/browser.png
new file mode 100644
index 0000000..b32717d
Binary files /dev/null and b/Spender/app/src/main/res/drawable/browser.png differ
diff --git a/Spender/app/src/main/res/drawable/cat.png b/Spender/app/src/main/res/drawable/cat.png
new file mode 100644
index 0000000..75c5833
Binary files /dev/null and b/Spender/app/src/main/res/drawable/cat.png differ
diff --git a/Spender/app/src/main/res/drawable/cheese.png b/Spender/app/src/main/res/drawable/cheese.png
new file mode 100644
index 0000000..a416d0c
Binary files /dev/null and b/Spender/app/src/main/res/drawable/cheese.png differ
diff --git a/Spender/app/src/main/res/drawable/clevercat.png b/Spender/app/src/main/res/drawable/clevercat.png
new file mode 100644
index 0000000..0f1dfe4
Binary files /dev/null and b/Spender/app/src/main/res/drawable/clevercat.png differ
diff --git a/Spender/app/src/main/res/drawable/delete.png b/Spender/app/src/main/res/drawable/delete.png
new file mode 100644
index 0000000..67980f2
Binary files /dev/null and b/Spender/app/src/main/res/drawable/delete.png differ
diff --git a/Spender/app/src/main/res/drawable/history.png b/Spender/app/src/main/res/drawable/history.png
new file mode 100644
index 0000000..7f3da04
Binary files /dev/null and b/Spender/app/src/main/res/drawable/history.png differ
diff --git a/Spender/app/src/main/res/drawable/history_chosen.png b/Spender/app/src/main/res/drawable/history_chosen.png
new file mode 100644
index 0000000..2bd816d
Binary files /dev/null and b/Spender/app/src/main/res/drawable/history_chosen.png differ
diff --git a/Spender/app/src/main/res/drawable/ic_launcher_background.xml b/Spender/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..0d025f9
--- /dev/null
+++ b/Spender/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Spender/app/src/main/res/drawable/icon.png b/Spender/app/src/main/res/drawable/icon.png
new file mode 100644
index 0000000..1374c27
Binary files /dev/null and b/Spender/app/src/main/res/drawable/icon.png differ
diff --git a/Spender/app/src/main/res/drawable/menu.png b/Spender/app/src/main/res/drawable/menu.png
new file mode 100644
index 0000000..3d2fad4
Binary files /dev/null and b/Spender/app/src/main/res/drawable/menu.png differ
diff --git a/Spender/app/src/main/res/drawable/piechart.png b/Spender/app/src/main/res/drawable/piechart.png
new file mode 100644
index 0000000..19675e3
Binary files /dev/null and b/Spender/app/src/main/res/drawable/piechart.png differ
diff --git a/Spender/app/src/main/res/drawable/piechart_chosen.png b/Spender/app/src/main/res/drawable/piechart_chosen.png
new file mode 100644
index 0000000..392c5e4
Binary files /dev/null and b/Spender/app/src/main/res/drawable/piechart_chosen.png differ
diff --git a/Spender/app/src/main/res/drawable/plus.png b/Spender/app/src/main/res/drawable/plus.png
new file mode 100644
index 0000000..a572685
Binary files /dev/null and b/Spender/app/src/main/res/drawable/plus.png differ
diff --git a/Spender/app/src/main/res/drawable/qrcode.png b/Spender/app/src/main/res/drawable/qrcode.png
new file mode 100644
index 0000000..165eca5
Binary files /dev/null and b/Spender/app/src/main/res/drawable/qrcode.png differ
diff --git a/Spender/app/src/main/res/drawable/stats.png b/Spender/app/src/main/res/drawable/stats.png
new file mode 100644
index 0000000..1149832
Binary files /dev/null and b/Spender/app/src/main/res/drawable/stats.png differ
diff --git a/Spender/app/src/main/res/drawable/tag.png b/Spender/app/src/main/res/drawable/tag.png
new file mode 100644
index 0000000..44a9aa8
Binary files /dev/null and b/Spender/app/src/main/res/drawable/tag.png differ
diff --git a/Spender/app/src/main/res/drawable/tag_add.png b/Spender/app/src/main/res/drawable/tag_add.png
new file mode 100644
index 0000000..bed7383
Binary files /dev/null and b/Spender/app/src/main/res/drawable/tag_add.png differ
diff --git a/Spender/app/src/main/res/drawable/tag_remove.png b/Spender/app/src/main/res/drawable/tag_remove.png
new file mode 100644
index 0000000..d211e64
Binary files /dev/null and b/Spender/app/src/main/res/drawable/tag_remove.png differ
diff --git a/Spender/app/src/main/res/font/bungee.xml b/Spender/app/src/main/res/font/bungee.xml
new file mode 100644
index 0000000..465b0f5
--- /dev/null
+++ b/Spender/app/src/main/res/font/bungee.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/Spender/app/src/main/res/font/ubuntu_light.xml b/Spender/app/src/main/res/font/ubuntu_light.xml
new file mode 100644
index 0000000..86ecb5a
--- /dev/null
+++ b/Spender/app/src/main/res/font/ubuntu_light.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/Spender/app/src/main/res/layout/activity_check_show.xml b/Spender/app/src/main/res/layout/activity_check_show.xml
new file mode 100644
index 0000000..d05ef47
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_check_show.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_help.xml b/Spender/app/src/main/res/layout/activity_help.xml
new file mode 100644
index 0000000..d4fa98b
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_help.xml
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_history.xml b/Spender/app/src/main/res/layout/activity_history.xml
new file mode 100644
index 0000000..da225bc
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_history.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_list.xml b/Spender/app/src/main/res/layout/activity_list.xml
new file mode 100644
index 0000000..1546ed2
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_list.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_login.xml b/Spender/app/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000..c5f3e58
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_main.xml b/Spender/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..89e9655
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_new_tag.xml b/Spender/app/src/main/res/layout/activity_new_tag.xml
new file mode 100644
index 0000000..571bdfc
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_new_tag.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_tag_choice.xml b/Spender/app/src/main/res/layout/activity_tag_choice.xml
new file mode 100644
index 0000000..0170098
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_tag_choice.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/activity_tag_list.xml b/Spender/app/src/main/res/layout/activity_tag_list.xml
new file mode 100644
index 0000000..42d540b
--- /dev/null
+++ b/Spender/app/src/main/res/layout/activity_tag_list.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/buttons.xml b/Spender/app/src/main/res/layout/buttons.xml
new file mode 100644
index 0000000..6249d1e
--- /dev/null
+++ b/Spender/app/src/main/res/layout/buttons.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/check_list_view.xml b/Spender/app/src/main/res/layout/check_list_view.xml
new file mode 100644
index 0000000..39883b7
--- /dev/null
+++ b/Spender/app/src/main/res/layout/check_list_view.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/fragment_line_chart.xml b/Spender/app/src/main/res/layout/fragment_line_chart.xml
new file mode 100644
index 0000000..6949a7c
--- /dev/null
+++ b/Spender/app/src/main/res/layout/fragment_line_chart.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/fragment_pie_chart.xml b/Spender/app/src/main/res/layout/fragment_pie_chart.xml
new file mode 100644
index 0000000..330227b
--- /dev/null
+++ b/Spender/app/src/main/res/layout/fragment_pie_chart.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/fragment_stacked_bar_chart.xml b/Spender/app/src/main/res/layout/fragment_stacked_bar_chart.xml
new file mode 100644
index 0000000..7b48517
--- /dev/null
+++ b/Spender/app/src/main/res/layout/fragment_stacked_bar_chart.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/history_item.xml b/Spender/app/src/main/res/layout/history_item.xml
new file mode 100644
index 0000000..7fe7116
--- /dev/null
+++ b/Spender/app/src/main/res/layout/history_item.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/item_view.xml b/Spender/app/src/main/res/layout/item_view.xml
new file mode 100644
index 0000000..b571112
--- /dev/null
+++ b/Spender/app/src/main/res/layout/item_view.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/list_view.xml b/Spender/app/src/main/res/layout/list_view.xml
new file mode 100644
index 0000000..2b0e7bd
--- /dev/null
+++ b/Spender/app/src/main/res/layout/list_view.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/Spender/app/src/main/res/layout/switch_item.xml b/Spender/app/src/main/res/layout/switch_item.xml
new file mode 100644
index 0000000..ee492c6
--- /dev/null
+++ b/Spender/app/src/main/res/layout/switch_item.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/Spender/app/src/main/res/layout/tag_choice_item_view.xml b/Spender/app/src/main/res/layout/tag_choice_item_view.xml
new file mode 100644
index 0000000..7651746
--- /dev/null
+++ b/Spender/app/src/main/res/layout/tag_choice_item_view.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/layout/tag_view.xml b/Spender/app/src/main/res/layout/tag_view.xml
new file mode 100644
index 0000000..ba26980
--- /dev/null
+++ b/Spender/app/src/main/res/layout/tag_view.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/menu/history_context_menu.xml b/Spender/app/src/main/res/menu/history_context_menu.xml
new file mode 100644
index 0000000..b3debf8
--- /dev/null
+++ b/Spender/app/src/main/res/menu/history_context_menu.xml
@@ -0,0 +1,9 @@
+
diff --git a/Spender/app/src/main/res/menu/history_menu.xml b/Spender/app/src/main/res/menu/history_menu.xml
new file mode 100644
index 0000000..f1bfa68
--- /dev/null
+++ b/Spender/app/src/main/res/menu/history_menu.xml
@@ -0,0 +1,9 @@
+
diff --git a/Spender/app/src/main/res/menu/list_view_context_menu.xml b/Spender/app/src/main/res/menu/list_view_context_menu.xml
new file mode 100644
index 0000000..037ec74
--- /dev/null
+++ b/Spender/app/src/main/res/menu/list_view_context_menu.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/menu/list_view_menu.xml b/Spender/app/src/main/res/menu/list_view_menu.xml
new file mode 100644
index 0000000..dcba0b1
--- /dev/null
+++ b/Spender/app/src/main/res/menu/list_view_menu.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/menu/products_menu.xml b/Spender/app/src/main/res/menu/products_menu.xml
new file mode 100644
index 0000000..0185289
--- /dev/null
+++ b/Spender/app/src/main/res/menu/products_menu.xml
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/menu/settings.xml b/Spender/app/src/main/res/menu/settings.xml
new file mode 100644
index 0000000..b019f4c
--- /dev/null
+++ b/Spender/app/src/main/res/menu/settings.xml
@@ -0,0 +1,78 @@
+
diff --git a/Spender/app/src/main/res/menu/tag_choice_menu.xml b/Spender/app/src/main/res/menu/tag_choice_menu.xml
new file mode 100644
index 0000000..db40ece
--- /dev/null
+++ b/Spender/app/src/main/res/menu/tag_choice_menu.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/menu/tag_list_context_menu.xml b/Spender/app/src/main/res/menu/tag_list_context_menu.xml
new file mode 100644
index 0000000..b3e38b5
--- /dev/null
+++ b/Spender/app/src/main/res/menu/tag_list_context_menu.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/menu/tag_list_menu.xml b/Spender/app/src/main/res/menu/tag_list_menu.xml
new file mode 100644
index 0000000..e7112a9
--- /dev/null
+++ b/Spender/app/src/main/res/menu/tag_list_menu.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/Spender/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Spender/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/Spender/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Spender/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/Spender/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Spender/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Spender/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Spender/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dffca36
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/Spender/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Spender/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Spender/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Spender/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dae5e08
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/Spender/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Spender/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed465
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Spender/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Spender/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..14ed0af
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/Spender/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Spender/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Spender/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Spender/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d8ae031
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/Spender/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Spender/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Spender/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Spender/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..beed3cd
Binary files /dev/null and b/Spender/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/Spender/app/src/main/res/values/colors.xml b/Spender/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..69b2233
--- /dev/null
+++ b/Spender/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/Spender/app/src/main/res/values/font_certs.xml b/Spender/app/src/main/res/values/font_certs.xml
new file mode 100644
index 0000000..d2226ac
--- /dev/null
+++ b/Spender/app/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/Spender/app/src/main/res/values/ids.xml b/Spender/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..55344e5
--- /dev/null
+++ b/Spender/app/src/main/res/values/ids.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/Spender/app/src/main/res/values/preloaded_fonts.xml b/Spender/app/src/main/res/values/preloaded_fonts.xml
new file mode 100644
index 0000000..2fc397f
--- /dev/null
+++ b/Spender/app/src/main/res/values/preloaded_fonts.xml
@@ -0,0 +1,7 @@
+
+
+
+ - @font/bungee
+ - @font/ubuntu_light
+
+
diff --git a/Spender/app/src/main/res/values/strings.xml b/Spender/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6bb4a38
--- /dev/null
+++ b/Spender/app/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+ Spender
+ Scan Code
+ Statistics
+ Spender
+ delete all purchases
+
+
+ Hello blank fragment
+
diff --git a/Spender/app/src/main/res/values/styles.xml b/Spender/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d69fe7f
--- /dev/null
+++ b/Spender/app/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Spender/app/src/test/java/com/project/spender/fns/api/FnsApiTest.java b/Spender/app/src/test/java/com/project/spender/fns/api/FnsApiTest.java
new file mode 100644
index 0000000..46bb6ae
--- /dev/null
+++ b/Spender/app/src/test/java/com/project/spender/fns/api/FnsApiTest.java
@@ -0,0 +1,105 @@
+package com.project.spender.fns.api;
+
+import com.project.spender.fns.api.data.Json.CheckJson;
+import com.project.spender.fns.api.data.Phone;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import okhttp3.Credentials;
+import okhttp3.ResponseBody;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+class FnsApiTest {
+ private Retrofit retrofit;
+ private FnsApi fns;
+ private String loginPassword;
+
+ private String fn;
+ private String fd;
+ private String fiscalSign;
+ private String date;
+ private String sum;
+
+ private String fn2;
+ private String fd2;
+ private String fiscalSign2;
+ private String date2;
+ private String sum2;
+
+ @BeforeEach
+ void init() {
+ retrofit = new Retrofit.Builder()
+ .baseUrl("https://proverkacheka.nalog.ru:9999") //Базовая часть адреса
+ .addConverterFactory(GsonConverterFactory.create()).build(); //Конвертер, необходимый для преобразования JSON'а в объекты
+ fns =retrofit.create(FnsApi.class);
+ loginPassword = Credentials.basic("+79112813247","583066");
+ fn = "9286000100242530";
+ fd = "27641";
+ fiscalSign = "124643923";
+ date = "20190402T1357";
+ sum = "21700";
+
+ fn2 = "9289000100349869";
+ fd2 = "6651";
+ fiscalSign2 = "2111509816";
+ date2 = "20190402T2121";
+ sum2 = "169268";
+ }
+
+ @Test
+ void isCheckExist() throws IOException {
+ fns.login(loginPassword).execute();
+ Response res = fns.isCheckExist(fn, fd, fiscalSign, date, sum).execute();
+ System.out.println(res.code());
+ System.out.println(res.message());
+ assertTrue(res.isSuccessful());
+ }
+
+ @Test
+ void login() throws IOException {
+ Response res = fns.login(loginPassword).execute();
+ System.out.println(res.code());
+ System.out.println(res.message());
+ assertTrue(res.isSuccessful());
+ }
+
+ @Test
+ void getCheck() throws IOException {
+ Response resExist = fns.isCheckExist(fn, fd, fiscalSign, date, sum).execute();
+ System.out.println(resExist.code());
+ System.out.println(resExist.message());
+
+ Response res = fns.getCheck(loginPassword, "", "",
+ fn, fd, fiscalSign, "no").execute();
+ System.out.println(res.code());
+ System.out.println(res.message());
+ System.out.println(res.body().getData().items.get(0).name);
+ assertTrue(res.isSuccessful());
+ }
+
+ @Test
+ void getRaw() throws IOException {
+ Response resExist = fns.isCheckExist(fn2, fd2, fiscalSign2, date2, sum2).execute();
+ Response res = fns.getRawCheck(loginPassword, "", "",
+ fn2, fd2, fiscalSign2, "no").execute();
+ System.out.println(res.body().string());
+ assertTrue(res.isSuccessful());
+ }
+
+ @Test
+ void restoreTest() throws IOException {
+ Response res = fns.restore(new Phone("+79113214567")).execute();
+ System.out.println(res.code());
+ System.out.println(res.message());
+ assertEquals(404, res.code());
+ }
+}
\ No newline at end of file
diff --git a/Spender/app/src/test/java/com/project/spender/fns/api/NetworkManagerTest.java b/Spender/app/src/test/java/com/project/spender/fns/api/NetworkManagerTest.java
new file mode 100644
index 0000000..e819f81
--- /dev/null
+++ b/Spender/app/src/test/java/com/project/spender/fns/api/NetworkManagerTest.java
@@ -0,0 +1,90 @@
+package com.project.spender.fns.api;
+
+import com.project.spender.data.ScanResult;
+import com.project.spender.fns.api.exception.NetworkException;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+
+public class NetworkManagerTest {
+ private ScanResult scanResult1;
+ private ScanResult scanResult2;
+ private ScanResult wrongResult;
+ private NetworkManager networkManager = new NetworkManager();
+
+ // все сломается если я поменяю пароль
+ private String defaultLogin = "+79112813247";
+ private String defaultPassword = "583066";
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Before
+ public void init() {
+ String fn1 = "9286000100242530";
+ String fd1 = "27641";
+ String fiscalSign1 = "124643923";
+ String date1 = "20190402T1357";
+ String sum1 = "21700";
+
+ scanResult1 = new ScanResult(fn1, fd1, fiscalSign1, date1, sum1);
+
+ String fn2 = "9289000100349869";
+ String fd2 = "6651";
+ String fiscalSign2 = "2111509816";
+ String date2 = "20190402T2121";
+ String sum2 = "169268";
+
+ scanResult2 = new ScanResult(fn2, fd2, fiscalSign2, date2, sum2);
+
+ wrongResult = new ScanResult(fn2, fd2, fiscalSign2 + "0", date2, sum2);
+ }
+
+ @Test
+ public void isCheckExistTrue1() throws IOException {
+ assertEquals(204, networkManager
+ .isCheckExistCodeSync(scanResult1));
+ }
+
+ @Test
+ public void isCheckExistTrue2() throws IOException {
+ assertEquals(204, networkManager
+ .isCheckExistCodeSync(scanResult2));
+ }
+
+ @Test
+ public void isCheckExistFalse() throws IOException {
+ assertNotEquals(204, networkManager
+ .isCheckExistCodeSync(wrongResult));
+ }
+
+ @Test
+ public void checkLoginTrue() throws IOException {
+ assertEquals(200, networkManager.checkUserSync(defaultLogin, defaultPassword));
+ }
+
+ @Test
+ public void checkLoginFalse() throws IOException {
+ assertNotEquals(200, networkManager.checkUserSync(defaultLogin, "1111"));
+ }
+
+ @Test
+ public void getCheck1() throws IOException, NetworkException {
+ assertEquals("ЧИК.МАКНАГГ. 9 БКОМБО", networkManager
+ .getCheckSync(defaultLogin, defaultPassword, scanResult1).getData().items.get(0).name);
+ }
+
+ @Test
+ public void getCheck2() throws IOException, NetworkException {
+ assertEquals("1:3305976 Пакет ПЕРЕКРЕСТОК майка 65х40см", networkManager
+ .getCheckSync(defaultLogin, defaultPassword, scanResult2).getData().items.get(0).name);
+ }
+
+}
\ No newline at end of file
diff --git a/Spender/build.gradle b/Spender/build.gradle
new file mode 100644
index 0000000..d105c6f
--- /dev/null
+++ b/Spender/build.gradle
@@ -0,0 +1,26 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.4.1'
+ classpath "de.mannodermaus.gradle.plugins:android-junit5:1.3.2.0"
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/Spender/gradle.properties b/Spender/gradle.properties
new file mode 100644
index 0000000..d546dea
--- /dev/null
+++ b/Spender/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+android.enableJetifier=true
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+
diff --git a/Spender/gradle/wrapper/gradle-wrapper.jar b/Spender/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..87b738c
Binary files /dev/null and b/Spender/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Spender/gradle/wrapper/gradle-wrapper.properties b/Spender/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c7b2b41
--- /dev/null
+++ b/Spender/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Jun 08 04:56:05 MSK 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/Spender/gradlew b/Spender/gradlew
new file mode 100644
index 0000000..af6708f
--- /dev/null
+++ b/Spender/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Spender/gradlew.bat b/Spender/gradlew.bat
new file mode 100644
index 0000000..0f8d593
--- /dev/null
+++ b/Spender/gradlew.bat
@@ -0,0 +1,84 @@
+@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
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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="-Xmx64m"
+
+@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 Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_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=%*
+
+: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/Spender/settings.gradle b/Spender/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/Spender/settings.gradle
@@ -0,0 +1 @@
+include ':app'