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