diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..60548c8 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,110 @@ +kind: pipeline +type: docker +name: default + +steps: +- name: build + image: nextcloudci/android:android-49 + volumes: + - name: gradle + path: /root/.gradle + - name: android + path: /opt/android-sdk-linux + commands: + - ./gradlew build + +- name: notify + image: appleboy/drone-telegram + settings: + token: + from_secret: telegram_token + to: + from_secret: telegram_user + format: markdown + message: | + Build [#{{build.number}}]({{build.link}}) from commit [{{truncate commit.sha 10}}]({{commit.link}}) on {{commit.branch}} **failed**. + {{uppercasefirst commit.author}} please fix me! + when: + status: + - failure + +volumes: +- name: gradle + host: + path: /home/drone/.gradle +- name: android + host: + path: /home/drone/Android/Sdk + +trigger: + event: + exclude: + - tag + +--- +kind: pipeline +type: docker +name: release + +steps: +- name: build + image: nextcloudci/android:android-49 + volumes: + - name: gradle + path: /root/.gradle + - name: android + path: /opt/android-sdk-linux + - name: tmp + path: /drone/src/out + commands: + - ./gradlew build + - ./gradlew :wrapper:fatJar + - mv /drone/src/wrapper/build/libs/*.jar /drone/src/out/ + - mv /drone/src/app/build/outputs/apk/release/*.apk /drone/src/out/ + +- name: gitea release + image: plugins/gitea-release + volumes: + - name: tmp + path: /drone/src/out + settings: + api_key: + from_secret: gitea_release + base_url: https://git.norangeb.it + title: wedroid + files: + - /drone/src/out/*.jar + - /drone/src/out/*.apk + checksum: + - md5 + - sha256 + +- name: notify + image: appleboy/drone-telegram + settings: + token: + from_secret: telegram_token + to: + from_secret: telegram_user + format: markdown + message: | + {{#success build.status}} + Release-{{build.tag}} successfully released. + {{else}} + Build [#{{build.number}}]({{build.link}}) from commit [{{truncate commit.sha 10}}]({{commit.link}}) with tag {{build.tag}} **failed**. + {{uppercasefirst commit.author}} please fix me! + {{/success}} + +volumes: +- name: tmp + temp: {} +- name: gradle + host: + path: /home/drone/.gradle +- name: android + host: + path: /home/drone/Android/Sdk + +trigger: + event: + - tag diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaba528 --- /dev/null +++ b/.gitignore @@ -0,0 +1,180 @@ + +# Created by https://www.gitignore.io/api/java,linux,gradle,windows,android +# Edit at https://www.gitignore.io/?templates=java,linux,gradle,windows,android + +### Android ### +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/uiDesigner.xml +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +### Android Patch ### +gen-external-apklibs +output.json + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. +.cxx/ + +### Java ### +# Compiled class file + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# End of https://www.gitignore.io/api/java,linux,gradle,windows,android diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..45b5654 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..6e6eec1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/GPL_v__2_0.xml b/.idea/copyright/GPL_v__2_0.xml new file mode 100644 index 0000000..6ac8137 --- /dev/null +++ b/.idea/copyright/GPL_v__2_0.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..29bb4c5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index eb76347..b8279d5 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,8 @@ Client Android per Wekan sviluppato per il progetto del corso magistrale Ingegneria del Software per l'Università del Sannio. +## Authors - - - - - - +- Furno Umberto +- Mignone Raffaele +- Mincolelli Noemi diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..4180076 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,78 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +apply plugin: 'kotlin-kapt' + +apply plugin: 'io.gitlab.arturbosch.detekt' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "it.unisannio.ding.ids.wedroid.app" + minSdkVersion 24 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + packagingOptions { + exclude 'META-INF/atomicfu.kotlin_module' + } +} + +dependencies { + // standard + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + // wrapper + implementation project(':wrapper') + // retrofit + implementation "com.squareup.retrofit2:converter-gson:2.6.2" + implementation "com.squareup.retrofit2:retrofit:2.6.2" + // room database + kapt "androidx.room:room-compiler:$rootProject.roomVersion" + implementation "androidx.room:room-ktx:$rootProject.roomVersion" + implementation "androidx.room:room-runtime:$rootProject.roomVersion" + // lifecycle components + kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion" + // viewmode kotlin support + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion" + // coroutines + api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines" + // UI + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + implementation 'androidx.cardview:cardview:1.0.0' + implementation "com.google.android.material:material:$rootProject.materialVersion" + + // testing + testImplementation "com.squareup.okhttp3:mockwebserver:4.2.1" + testImplementation "io.mockk:mockk:1.9.3" + testImplementation 'junit:junit:4.12' + + // android testing + androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion" + androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion" + androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion" + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + + // detekt + detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:1.2.2" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/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/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/TestHelper.kt b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/TestHelper.kt new file mode 100644 index 0000000..017874f --- /dev/null +++ b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/TestHelper.kt @@ -0,0 +1,22 @@ +package it.unisannio.ding.ids.wedroid.app + +import androidx.lifecycle.* + +class OneTimeObserver(private val handler: (T) -> Unit) : Observer, LifecycleOwner { + private val lifecycle = LifecycleRegistry(this) + init { + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun onChanged(t: T) { + handler(t) + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } +} + +fun LiveData.observeOnce(onChangeHandler: (T) -> Unit) { + val observer = OneTimeObserver(handler = onChangeHandler) + observe(observer, observer) +} diff --git a/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDaoTest.kt b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDaoTest.kt new file mode 100644 index 0000000..e6151bb --- /dev/null +++ b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDaoTest.kt @@ -0,0 +1,110 @@ +package it.unisannio.ding.ids.wedroid.app.data.dao + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import it.unisannio.ding.ids.wedroid.app.data.database.BoardDatabase +import it.unisannio.ding.ids.wedroid.app.data.entity.Board +import it.unisannio.ding.ids.wedroid.app.observeOnce +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class BoardDaoTest { + private lateinit var dao: BoardDao + private lateinit var db: BoardDatabase + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, BoardDatabase::class.java + ).build() + dao = db.boardDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + fun emptyDatabaseOnCreation() { + dao.getAllBoard().observeOnce { + assertEquals(0, it.size) + } + } + + @Test + fun insert() { + val board = Board("id", "title") + + runBlocking { + dao.insert(board) + } + + dao.getAllBoard().observeOnce { + assertEquals(1, it.size) + assertEquals(board, it[0]) + } + } + + @Test + fun replaceOnConflict() { + val board0 = Board("id", "title0") + val board1 = Board("id", "title1") + + runBlocking { + dao.insert(board0) + dao.insert(board1) + } + + dao.getAllBoard().observeOnce { + assertEquals(1, it.size) + assertEquals("title1", it[0].title) + } + } + + @Test + fun getInAscendingOrder() { + val board0 = Board("id0", "title0") + val board1 = Board("id1", "title1") + + runBlocking { + dao.insert(board1) + dao.insert(board0) + } + + dao.getAllBoard().observeOnce { + assertEquals(2, it.size) + assertEquals(board0, it[0]) + assertEquals(board1, it[1]) + } + } + + @Test + fun delete() { + val board = Board("id", "title") + + runBlocking { + dao.insert(board) + dao.delete(board) + } + + dao.getAllBoard().observeOnce { + assertEquals(0, it.size) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/data/dao/WListDaoTest.kt b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/data/dao/WListDaoTest.kt new file mode 100644 index 0000000..8e9096d --- /dev/null +++ b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/data/dao/WListDaoTest.kt @@ -0,0 +1,111 @@ +package it.unisannio.ding.ids.wedroid.app.data.dao + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import it.unisannio.ding.ids.wedroid.app.data.database.WListDatabase +import it.unisannio.ding.ids.wedroid.app.data.entity.WList +import it.unisannio.ding.ids.wedroid.app.observeOnce +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class WListDaoTest { + private lateinit var dao: WListDao + private lateinit var db: WListDatabase + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, WListDatabase::class.java + ).build() + dao = db.wListDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + fun emptyDatabaseOnCreation() { + dao.allWList.observeOnce { + assertEquals(0, it.size) + } + } + + @Test + fun insert() { + val wList = WList("id", "title") + + runBlocking { + dao.insert(wList) + } + + dao.allWList.observeOnce { + assertEquals(1, it.size) + assertEquals(wList, it[0]) + } + } + + @Test + fun replaceOnConflict() { + val wList0 = WList("id", "title0") + val wList1 = WList("id", "title1") + + runBlocking { + dao.insert(wList0) + dao.insert(wList1) + } + + dao.allWList.observeOnce { + assertEquals(1, it.size) + assertEquals("title1", it[0].title) + } + } + + @Test + fun getInAscendingOrder() { + val wList0 = WList("id0", "title0") + val wList1 = WList("id1", "title1") + + runBlocking { + dao.insert(wList1) + dao.insert(wList0) + } + + dao.allWList.observeOnce { + assertEquals(2, it.size) + assertEquals(wList0, it[0]) + assertEquals(wList1, it[1]) + } + } + + @Test + fun delete() { + val wlist = WList("id", "title") + + runBlocking { + dao.insert(wlist) + dao.delete(wlist) + } + + dao.allWList.observeOnce { + assertEquals(0, it.size) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivityTest.kt b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivityTest.kt new file mode 100644 index 0000000..6fe7e0a --- /dev/null +++ b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivityTest.kt @@ -0,0 +1,52 @@ +package it.unisannio.ding.ids.wedroid.app.view + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.action.ViewActions.swipeLeft +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.rule.ActivityTestRule +import it.unisannio.ding.ids.wedroid.app.R +import it.unisannio.ding.ids.wedroid.app.view.adapter.BoardsListAdapter +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class BoardsListsActivityTest { + + @get:Rule + val activityRule = ActivityTestRule(BoardsListsActivity::class.java) + + fun swipeLeftToDelete() { + onView(withId(R.id.boardList)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, swipeLeft() + ) + ) + } + + fun pullToRefresh() { + onView(withId(R.id.pullToRefresh)) + .perform(swipeDown()) + + } + + @Test + fun openNewBoardActivity() { + onView(withId(R.id.fab)) + .perform(click()) + + onView(withId(R.id.newBoardName)) + .check(matches(isDisplayed())) + } + +} + diff --git a/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/LoginActivityTest.kt b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/LoginActivityTest.kt new file mode 100644 index 0000000..62af7b7 --- /dev/null +++ b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/LoginActivityTest.kt @@ -0,0 +1,87 @@ +package it.unisannio.ding.ids.wedroid.app.view + +import androidx.test.espresso.Espresso.closeSoftKeyboard +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.rule.ActivityTestRule +import it.unisannio.ding.ids.wedroid.app.R +import org.hamcrest.CoreMatchers.not +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class LoginActivityTest { + @get:Rule + val activityRule = ActivityTestRule(LoginActivity::class.java) + + @Test + fun loginWithEmptyUrl() { + onView(withId(R.id.username)) + .perform(typeText("username")) + onView(withId(R.id.password)) + .perform(typeText("password")) + closeSoftKeyboard() + onView(withId(R.id.button)) + .perform(click()) + + onView(withText(R.string.login_empty_field)) + .inRoot(withDecorView(not(activityRule.activity.window.decorView))) + .check(matches(isDisplayed())) + } + + @Test + fun loginWithEmptyUsername() { + onView(withId(R.id.instanceServer)) + .perform(typeText("https://wekan.com")) + onView(withId(R.id.password)) + .perform(typeText("password")) + closeSoftKeyboard() + onView(withId(R.id.button)) + .perform(click()) + + onView(withText(R.string.login_empty_field)) + .inRoot(withDecorView(not(activityRule.activity.window.decorView))) + .check(matches(isDisplayed())) + + } + + @Test + fun loginWithEmptyPassword() { + onView(withId(R.id.instanceServer)) + .perform(typeText("https://wekan.com")) + onView(withId(R.id.username)) + .perform(typeText("username")) + closeSoftKeyboard() + onView(withId(R.id.button)) + .perform(click()) + + onView(withText(R.string.login_empty_field)) + .inRoot(withDecorView(not(activityRule.activity.window.decorView))) + .check(matches(isDisplayed())) + } + + @Test + fun loginWithUnformedInstance(){ + onView(withId(R.id.instanceServer)) + .perform(typeText("not an URL")) + onView(withId(R.id.username)) + .perform(typeText("username")) + onView(withId(R.id.password)) + .perform(typeText("password")) + closeSoftKeyboard() + onView(withId(R.id.button)) + .perform(click()) + + onView(withText(R.string.login_unformed_instance)) + .inRoot(withDecorView(not(activityRule.activity.window.decorView))) + .check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/NewBoardActivityTest.kt b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/NewBoardActivityTest.kt new file mode 100644 index 0000000..4f49761 --- /dev/null +++ b/app/src/androidTest/java/it/unisannio/ding/ids/wedroid/app/view/NewBoardActivityTest.kt @@ -0,0 +1,37 @@ +package it.unisannio.ding.ids.wedroid.app.view + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.rule.ActivityTestRule +import it.unisannio.ding.ids.wedroid.app.R +import org.hamcrest.core.IsNot.not +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class NewBoardActivityTest { + + @get:Rule + val activityRule = ActivityTestRule(NewBoardActivity::class.java) + + @Test + fun showToastOnEmptyName() { + onView(withId(R.id.newBoardDone)) + .perform(click()) + + onView(withText(R.string.on_add_new_board_empty_name)) + .inRoot(withDecorView(not(activityRule.activity.window.decorView))) + .check(matches(isDisplayed())) + } + +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d6d9155 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt new file mode 100644 index 0000000..adcfa85 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt @@ -0,0 +1,21 @@ +package it.unisannio.ding.ids.wedroid.app.data.dao + +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 it.unisannio.ding.ids.wedroid.app.data.entity.Board + +@Dao +interface BoardDao { + @Query("SELECT * from board_table ORDER BY title ASC") + fun getAllBoard(): LiveData> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(board: Board) + + @Delete + suspend fun delete(board: Board) +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/WListDao.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/WListDao.java new file mode 100644 index 0000000..3c74fe7 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/WListDao.java @@ -0,0 +1,27 @@ +package it.unisannio.ding.ids.wedroid.app.data.dao; + +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 java.util.List; + +//import io.reactivex.Completable; +import it.unisannio.ding.ids.wedroid.app.data.entity.WList; + +@Dao +public interface WListDao { + + @Query("SELECT * from wlist_table ORDER BY title ASC") + LiveData> getAllWList(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(WList wList); + + @Delete + void delete(WList wList); + +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.java new file mode 100644 index 0000000..b9c6021 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.java @@ -0,0 +1,31 @@ +package it.unisannio.ding.ids.wedroid.app.data.database; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import it.unisannio.ding.ids.wedroid.app.data.dao.BoardDao; +import it.unisannio.ding.ids.wedroid.app.data.entity.Board; + +@Database(entities = Board.class, version = 1, exportSchema = false) +public abstract class BoardDatabase extends RoomDatabase { + private static volatile BoardDatabase INSTANCE; + + public abstract BoardDao boardDao(); + + public static BoardDatabase getDatabase(Context context) { + if (INSTANCE != null) + return INSTANCE; + synchronized (BoardDatabase.class) { + INSTANCE = Room.databaseBuilder( + context.getApplicationContext(), + BoardDatabase.class, + "board_database" + ).build(); + + return INSTANCE; + } + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/WListDatabase.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/WListDatabase.java new file mode 100644 index 0000000..f35da1e --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/WListDatabase.java @@ -0,0 +1,31 @@ +package it.unisannio.ding.ids.wedroid.app.data.database; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import it.unisannio.ding.ids.wedroid.app.data.dao.WListDao; +import it.unisannio.ding.ids.wedroid.app.data.entity.WList; + +@Database(entities = WList.class, version = 2, exportSchema = false) +public abstract class WListDatabase extends RoomDatabase { + private static volatile WListDatabase INSTANCE; + + public abstract WListDao wListDao(); + + public static WListDatabase getDatabase(Context context) { + if (INSTANCE != null) + return INSTANCE; + synchronized (WListDatabase.class) { + INSTANCE = Room.databaseBuilder( + context.getApplicationContext(), + WListDatabase.class, + "wlist_database" + ).fallbackToDestructiveMigration().build(); + + return INSTANCE; + } + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt new file mode 100644 index 0000000..eb61071 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt @@ -0,0 +1,15 @@ +package it.unisannio.ding.ids.wedroid.app.data.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "board_table") +data class Board( + @PrimaryKey @ColumnInfo(name = "id") val id: String, + @ColumnInfo(name = "title") val title: String = "" +) + +fun it.unisannio.ding.ids.wedroid.wrapper.entity.Board.convert(): Board { + return Board(this.id, this.title) +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/WList.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/WList.kt new file mode 100644 index 0000000..1fedd70 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/WList.kt @@ -0,0 +1,15 @@ +package it.unisannio.ding.ids.wedroid.app.data.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "wlist_table") +data class WList( + @PrimaryKey @ColumnInfo(name = "id") val id: String, + @ColumnInfo(name = "title") val title: String = "" +) + +fun it.unisannio.ding.ids.wedroid.wrapper.entity.WList.convert(): WList { + return WList(this.id, this.title) +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt new file mode 100644 index 0000000..0c9e4de --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt @@ -0,0 +1,161 @@ +package it.unisannio.ding.ids.wedroid.app.data.repository + +import android.util.Log +import it.unisannio.ding.ids.wedroid.app.data.dao.BoardDao +import it.unisannio.ding.ids.wedroid.app.data.entity.Board +import it.unisannio.ding.ids.wedroid.app.data.entity.convert +import it.unisannio.ding.ids.wedroid.app.util.PreferenceReader +import it.unisannio.ding.ids.wedroid.wrapper.api.BoardService +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardBackgroundColor +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardPermission +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardPrototype +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class BoardRepository( + private val dao: BoardDao, + private val service: BoardService, + private val reader: PreferenceReader +) { + val allBoards by lazy { + dao.getAllBoard() + } + + init { + synchronize() + } + + fun synchronize() { + service.getBoardsFromUser(reader.userId) + .enqueue(object : + Callback> { + override fun onFailure( + call: Call>, + t: Throwable + ) = logNetworkError(t.message) + + override fun onResponse( + call: Call>, + response: Response> + ) { + CoroutineScope(Dispatchers.IO).launch { + synchronizeCallback(response) + } + } + }) + } + + private suspend fun synchronizeCallback( + response: Response> + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + + // read boards from the body + val boards = (response.body() ?: return) + .map { it.convert() } + + addNewBoardToDb(boards) + + removeOldBoardsFromDb(boards) + } + + fun insertBoard(title: String, isPrivate: Boolean, color: BoardBackgroundColor) { + val permission = if (isPrivate) BoardPermission.PRIVATE else BoardPermission.PUBLIC + + service.newBoard( + BoardPrototype.Builder() + .setOwner(reader.userId) + .setTitle(title) + .setBackgroundColor(color) + .setBoardPermission(permission) + .build() + ).enqueue(object : Callback { + override fun onFailure( + call: Call, + t: Throwable + ) = logNetworkError(t.message) + + override fun onResponse( + call: Call, + response: Response + ) { + CoroutineScope(Dispatchers.IO).launch { + insertBoardCallback(response, title) + } + } + }) + } + + private suspend fun insertBoardCallback( + response: Response, + title: String + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + + val board = response.body() + + if (board == null) { + logNetworkError("empty body") + return + } + + dao.insert(Board(board.id, title)) + } + + fun deleteBoard(id: String) { + service.deleteBoard(id).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + logNetworkError(t.message) + } + + override fun onResponse( + call: Call, + response: Response + ) { + CoroutineScope(Dispatchers.IO).launch { + deleteBoardCallback(response, id) + } + } + }) + } + + private suspend fun deleteBoardCallback( + response: Response, + id: String + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + + dao.delete(Board(id)) + } + + private suspend fun addNewBoardToDb(boards: Collection) { + boards.forEach { + dao.insert(it) + } + } + + private suspend fun removeOldBoardsFromDb(boards: Collection) { + allBoards.value?.minus(boards) + ?.forEach { + dao.delete(it) + } + } + + private fun logNetworkError(message: String?) { + Log.e("RETROFIT", message) + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/WListRepository.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/WListRepository.kt new file mode 100644 index 0000000..c377bbc --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/WListRepository.kt @@ -0,0 +1,116 @@ +package it.unisannio.ding.ids.wedroid.app.data.repository + +import android.util.Log + +import androidx.lifecycle.LiveData +import it.unisannio.ding.ids.wedroid.app.data.dao.WListDao +import it.unisannio.ding.ids.wedroid.app.data.entity.WList +import it.unisannio.ding.ids.wedroid.app.data.entity.convert +import it.unisannio.ding.ids.wedroid.app.util.PreferenceReader +import it.unisannio.ding.ids.wedroid.wrapper.api.ListService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class WListRepository( + private val dao: WListDao, + private val service: ListService, + private val reader: PreferenceReader +) { + + val allWLists: LiveData> by lazy { + dao.allWList + } + + init { + synchronize() + } + + fun synchronize() { + service.getAllList(reader.boardId) + .enqueue(object : + Callback> { + override fun onFailure( + call: Call>, + t: Throwable + ) = logNetworkError(t.message) + + override fun onResponse( + call: Call>, + response: Response> + ) { + CoroutineScope(Dispatchers.IO).launch { + synchronizeCallback(response) + } + } + }) + } + + private fun synchronizeCallback( + response: Response> + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + + val wLists = (response.body() ?: return) + .map { it.convert() } + + addNewWListToDb(wLists) + removeOldWListsFromDb(wLists) + } + + fun deleteWList(idBoard: String, idWList: String) { + service.deleteList(idBoard, idWList).enqueue( + object : Callback { + override fun onFailure( + call: Call, + t: Throwable + ) { + logNetworkError(t.message) + } + + override fun onResponse( + call: Call, + response: Response + ) { + CoroutineScope(Dispatchers.IO).launch { + deleteWListCallBack(response, idWList) + } + } + }) + } + + private fun deleteWListCallBack( + response: Response, + id: String + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + dao.delete(WList(id)) + } + + private fun addNewWListToDb(wLists: Collection) { + wLists.forEach { + dao.insert(it) + } + } + + private fun removeOldWListsFromDb(wLists: Collection) { + allWLists.value?.minus(wLists) + ?.forEach { + dao.delete(it) + } + } + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + private fun logNetworkError(message: String?) { + Log.e("RETROFIT", message) + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/PreferenceReader.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/PreferenceReader.java new file mode 100644 index 0000000..2574e01 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/PreferenceReader.java @@ -0,0 +1,11 @@ +package it.unisannio.ding.ids.wedroid.app.util; + +public interface PreferenceReader { + String getBaseUrl(); + + String getUserId(); + + String getToken(); + + String getBoardId(); +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/PreferenceWriter.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/PreferenceWriter.java new file mode 100644 index 0000000..74685c8 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/PreferenceWriter.java @@ -0,0 +1,11 @@ +package it.unisannio.ding.ids.wedroid.app.util; + +public interface PreferenceWriter { + void setBaseUrl(String baseUrl); + + void setUserId(String userId); + + void setToken(String token); + + void setBoardId(String boardId); +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/ServicesFactory.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/ServicesFactory.kt new file mode 100644 index 0000000..c3c3a83 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/ServicesFactory.kt @@ -0,0 +1,83 @@ +package it.unisannio.ding.ids.wedroid.app.util + +import it.unisannio.ding.ids.wedroid.wrapper.api.BoardService +import it.unisannio.ding.ids.wedroid.wrapper.api.CardCommentService +import it.unisannio.ding.ids.wedroid.wrapper.api.CardService +import it.unisannio.ding.ids.wedroid.wrapper.api.ChecklistService +import it.unisannio.ding.ids.wedroid.wrapper.api.UserService +import it.unisannio.ding.ids.wedroid.wrapper.api.ListService +import it.unisannio.ding.ids.wedroid.wrapper.api.SwimlanesService +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class ServicesFactory( + reader: PreferenceReader +) { + private val retrofit: Retrofit + + init { + val httpClient = OkHttpClient.Builder() + .addInterceptor { + val request = it.request().newBuilder() + .addHeader("Authorization", "Bearer ${reader.token}") + .addHeader("Accept", "application/json") + .addHeader("Content-type", "application/json") + .build() + + it.proceed(request) + }.build() + + retrofit = Retrofit.Builder() + .baseUrl(reader.baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient) + .build() + } + + val boardService by lazy { + retrofit.create(BoardService::class.java) + } + + val cardCommentService by lazy { + retrofit.create(CardCommentService::class.java) + } + + val cardService by lazy { + retrofit.create(CardService::class.java) + } + + val checklistService by lazy { + retrofit.create(ChecklistService::class.java) + } + + val listService by lazy { + retrofit.create(ListService::class.java) + } + + val swimlanesService by lazy { + retrofit.create(SwimlanesService::class.java) + } + + val userService by lazy { + retrofit.create(UserService::class.java) + } + + companion object { + @Volatile + private var INSTANCE: ServicesFactory? = null + + fun getInstance(reader: PreferenceReader): ServicesFactory { + val tempInstance = INSTANCE + + if (tempInstance != null) + return tempInstance + + synchronized(this) { + val instance = ServicesFactory(reader) + INSTANCE = instance + return instance + } + } + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/SharedPreferenceHelper.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/SharedPreferenceHelper.kt new file mode 100644 index 0000000..e69ca56 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/util/SharedPreferenceHelper.kt @@ -0,0 +1,43 @@ +package it.unisannio.ding.ids.wedroid.app.util + +import android.content.Context + +class SharedPreferenceHelper(context: Context) : PreferenceReader, PreferenceWriter { + private val sp = context.getSharedPreferences("userinfo", Context.MODE_PRIVATE) + + override fun getBaseUrl(): String? { + return sp.getString("url", "") + } + + override fun getUserId(): String? { + return sp.getString("id", "") + } + + override fun getToken(): String? { + return sp.getString("token", "") + } + + override fun setBaseUrl(baseUrl: String?) { + val editor = sp.edit() + editor.putString("url", baseUrl).apply() + } + + override fun setUserId(userId: String?) { + val editor = sp.edit() + editor.putString("id", userId).apply() + } + + override fun setToken(token: String?) { + val editor = sp.edit() + editor.putString("token", token).apply() + } + + override fun getBoardId(): String? { + return sp.getString("boardId", "") + } + + override fun setBoardId(token: String?) { + val editor = sp.edit() + editor.putString("boardId", token).apply() + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardViewActivity.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardViewActivity.java new file mode 100644 index 0000000..2b147a9 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardViewActivity.java @@ -0,0 +1,231 @@ +package it.unisannio.ding.ids.wedroid.app.view; + +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Objects; + +import it.unisannio.ding.ids.wedroid.app.R; +import it.unisannio.ding.ids.wedroid.app.util.ServicesFactory; +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper; +import it.unisannio.ding.ids.wedroid.wrapper.entity.Board; +import it.unisannio.ding.ids.wedroid.wrapper.entity.User; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class BoardViewActivity extends AppCompatActivity { + + static final int WLISTS_REQUEST = 30; + + private String idBoard, username, boardTitle; + private int boardColor; + private TextView description, members, permission, creationDate, lastModificationDate; + private View divider1, divider2, divider3; + private ListView listView; + private Button getListsButton; + private SharedPreferenceHelper sp; + private ServicesFactory service; + private Toolbar myToolbar; + private Board board; + private SwipeRefreshLayout swipeRefreshLayout; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_board_view); + + findViews(); + + setSupportActionBar(myToolbar); + + swipeRefreshLayout.setRefreshing(false); + + Intent i = getIntent(); + idBoard = i.getStringExtra("idBoard"); + + sp = new SharedPreferenceHelper(this); + sp.setBoardId(idBoard); + + initializeUI(idBoard); + + getListsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent i = new Intent(getApplicationContext(), WListsListActivity.class); + i.putExtra("idBoard", idBoard); + i.putExtra("barTitle", boardTitle); + i.putExtra("barColor", boardColor); + startActivityForResult(i, WLISTS_REQUEST); + } + }); + + swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + initializeUI(idBoard); + swipeRefreshLayout.setRefreshing(false); + } + }); + } + + private void findViews() { + myToolbar = findViewById(R.id.my_toolbar); + getListsButton = findViewById(R.id.getLists); + description = findViewById(R.id.descriptionTxt); + members = findViewById(R.id.membersTxt); + permission = findViewById(R.id.permissionTxt); + creationDate = findViewById(R.id.createdDate); + lastModificationDate = findViewById(R.id.modifiedDate); + divider1 = findViewById(R.id.divider1); + divider2 = findViewById(R.id.divider2); + divider3 = findViewById(R.id.divider3); + listView = findViewById(R.id.listViewID); + swipeRefreshLayout = findViewById(R.id.pullToRefresh); + } + + private void initializeUI(String idBoard) { + service = new ServicesFactory(sp); + service.getBoardService().getBoard(idBoard).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + assert response.body() != null; + board = response.body(); + if (board.getId() == null) { + Toast.makeText( + getApplicationContext(), + getApplicationContext().getString(R.string.board_deleted), + Toast.LENGTH_LONG) + .show(); + getListsButton.setEnabled(false); + } else { + Objects.requireNonNull(getSupportActionBar()).setTitle(board.getTitle()); + myToolbar.setBackgroundColor(Color.parseColor(encodeColor( + board.getBackgroundColor().toString()))); + + boardTitle = board.getTitle(); + + boardColor = Color.parseColor(encodeColor( + board.getBackgroundColor().toString())); + + Drawable background = getListsButton.getBackground(); + background.setTint(boardColor); + getListsButton.setBackgroundDrawable(background); + + description.setText(board.getDescription()); + permission.setText(board.getPermission().toString()); + members.setText(""); + for (int i = 0; i < board.getMembers().size(); i++) { + replaceIDUserToUsername(board.getMembers().get(i).getUserId()); + } + + creationDate.setText(R.string.created_at); + creationDate.append("\n" + + board.getCreatedAt()); + lastModificationDate.setText(R.string.modified_at); + lastModificationDate.append("\n" + + board.getModifiedAt()); + + divider1.setBackgroundColor(boardColor); + divider2.setBackgroundColor(boardColor); + divider3.setBackgroundColor(boardColor); + + ArrayList labelsTitle = new ArrayList<>(); + for (int i = 0; i < board.getLabels().size(); i++) { + labelsTitle.add(board.getLabels().get(i).getName()); + } + ArrayAdapter adapter = new ArrayAdapter<>(getApplicationContext(), + android.R.layout.simple_list_item_1, labelsTitle); + listView.setAdapter(adapter); + getListsButton.setEnabled(true); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull Throwable t) { + Toast.makeText(getApplicationContext(), + "connection error", + Toast.LENGTH_LONG).show(); + getListsButton.setEnabled(false); + } + }); + } + + private String encodeColor(String color) { + String encodedColor; + if (color.equalsIgnoreCase("belize")) { + encodedColor = "#2980B9"; + } else if (color.equalsIgnoreCase("nephritis")) { + encodedColor = "#27AE60"; + } else if (color.equalsIgnoreCase("pomegranate")) { + encodedColor = "#C0392B"; + } else if (color.equalsIgnoreCase("pumpkin")) { + encodedColor = "#E67E22"; + } else if (color.equalsIgnoreCase("wisteria")) { + encodedColor = "#8E44AD"; + } else if (color.equalsIgnoreCase("moderatepink")) { + encodedColor = "#CD5A91"; + } else if (color.equalsIgnoreCase("strongcyan")) { + encodedColor = "#00AECC"; + } else if (color.equalsIgnoreCase("dark")) { + encodedColor = "#2C3E51"; + } else if (color.equalsIgnoreCase("midnight")) { + encodedColor = "#2C3E50"; + } else if (color.equalsIgnoreCase("relax")) { + encodedColor = "#27AE61"; + } else if (color.equalsIgnoreCase("corteza")) { + encodedColor = "#568BA2"; + } else + encodedColor = "#38DF87"; + + return encodedColor; + } + + private void replaceIDUserToUsername(String idUser) { + service = new ServicesFactory(sp); + service.getUserService().getUser(idUser).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + User u = response.body(); + assert u != null; + username = u.getUsername(); + if (u.isAdmin()) { + members.append("Admin: " + username + ";\n"); + } else + members.append("Other member: " + username + ";\n"); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull Throwable t) { + //TODO + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == WLISTS_REQUEST) { + if (resultCode != WListsListActivity.RESULT_OK) { + Toast.makeText(this, "It's not possible view the lists", + Toast.LENGTH_LONG).show(); + } + } + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt new file mode 100644 index 0000000..d3c6924 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt @@ -0,0 +1,133 @@ +package it.unisannio.ding.ids.wedroid.app.view + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import it.unisannio.ding.ids.wedroid.app.R +import it.unisannio.ding.ids.wedroid.app.util.PreferenceReader +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper +import it.unisannio.ding.ids.wedroid.app.view.adapter.BoardsListAdapter +import it.unisannio.ding.ids.wedroid.app.viewmodel.BoardsListViewModel +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardBackgroundColor + +import kotlinx.android.synthetic.main.activity_boards_lists.* +import kotlinx.android.synthetic.main.content_boards_lists.* +import java.util.Locale + +class BoardsListsActivity : AppCompatActivity() { + private lateinit var viewModel: BoardsListViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_boards_lists) + setSupportActionBar(toolbar) + val reader: PreferenceReader = SharedPreferenceHelper(this) + + if (reader.token == "") + startActivityForResult( + Intent(this, LoginActivity::class.java), + LOGIN_CODE + ) + else initializeUi() + } + + private fun initializeUi() { + viewModel = ViewModelProvider(this).get(BoardsListViewModel::class.java) + + val adapter = BoardsListAdapter(this) + boardList.adapter = adapter + boardList.layoutManager = LinearLayoutManager(this) + + viewModel.allBoards.observe(this, Observer { + it.let { adapter.setBoards(it) } + pullToRefresh.isRefreshing = false + }) + + swipeLeftToDelete() + + fab.setOnClickListener { + startActivityForResult( + Intent(this, NewBoardActivity::class.java), + NEW_BOARD_CODE + ) + } + + pullToRefresh.setColorSchemeColors(getColor(R.color.colorAccent)) + pullToRefresh.setOnRefreshListener { + viewModel.refresh() + } + } + + private fun swipeLeftToDelete() { + val swipeToDelete = ItemTouchHelper( + object : ItemTouchHelper.SimpleCallback( + 0, ItemTouchHelper.LEFT + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val pos = viewHolder.adapterPosition + viewModel.deleteBoard(pos) + } + }) + + swipeToDelete.attachToRecyclerView(boardList) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + LOGIN_CODE -> onLoginResult(resultCode) + NEW_BOARD_CODE -> if (data != null) onAddBoardResult(resultCode, data) + else -> finish() + } + } + + private fun onLoginResult(resultCode: Int) { + when (resultCode) { + LoginActivity.LOGIN_OK -> initializeUi() + LoginActivity.LOGIN_ERROR -> finish() + else -> finish() + } + } + + private fun onAddBoardResult(resultCode: Int, data: Intent) { + if (resultCode != NewBoardActivity.RESULT_OK) { + Toast.makeText(this, R.string.on_add_new_board_error, Toast.LENGTH_LONG) + .show() + return + } + + val title = data.getStringExtra(NewBoardActivity.BOARD_NAME) + if (title == null) { + Toast.makeText(this, R.string.on_null_new_board_name, Toast.LENGTH_LONG) + .show() + return + } + + val isPrivate = data.getBooleanExtra(NewBoardActivity.BOARD_PRIVATE, true) + + val colorName = data.getStringExtra(NewBoardActivity.BOARD_BACKGROUND_COLOR) ?: "belize" + val backgroundColor = BoardBackgroundColor + .valueOf(colorName.toUpperCase(Locale.ROOT)) + + viewModel.insertBoard(title, isPrivate, backgroundColor) + } + + companion object { + const val NEW_BOARD_CODE = 17 + const val LOGIN_CODE = 19 + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/LoginActivity.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/LoginActivity.kt new file mode 100644 index 0000000..d7405ff --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/LoginActivity.kt @@ -0,0 +1,98 @@ +package it.unisannio.ding.ids.wedroid.app.view + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.View +import android.webkit.URLUtil +import android.widget.Toast +import it.unisannio.ding.ids.wedroid.app.R +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper +import it.unisannio.ding.ids.wedroid.wrapper.api.LoginService +import it.unisannio.ding.ids.wedroid.wrapper.entity.UserPrototype +import kotlinx.android.synthetic.main.activity_login.* +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class LoginActivity : AppCompatActivity() { + lateinit var sph: SharedPreferenceHelper + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + sph = SharedPreferenceHelper(this) + + setResult(LOGIN_ERROR) + + val id = sph.userId + val token = sph.token + val url = sph.baseUrl + } + + fun loginButton(v: View) { + if (username.text.isBlank() || instanceServer.text.isBlank() || password.text.isBlank()) { + Toast.makeText(this, R.string.login_empty_field, Toast.LENGTH_LONG) + .show() + return + } + + val userNameText = username.text.toString() + val passwordText = password.text.toString() + val instanceServerText = instanceServer.text.toString() + + if (!URLUtil.isValidUrl(instanceServerText)) { + Toast.makeText(this, R.string.login_unformed_instance, Toast.LENGTH_LONG) + .show() + return + } + + val service = Retrofit.Builder() + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl(instanceServerText) + .build() + .create(LoginService::class.java) + + service.login(userNameText, passwordText).enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText( + applicationContext, + R.string.login_network_error, + Toast.LENGTH_LONG + ).show() + } + + override fun onResponse(call: Call, response: Response) { + + if (response.code() != HTTP_CODE_OK) { + Toast.makeText( + applicationContext, + R.string.login_wrong_field, + Toast.LENGTH_LONG + ).show() + return + } + val users = response.body() + sph.baseUrl = instanceServer.text.toString() + sph.token = users?.token + sph.userId = users?.id + + Toast.makeText( + applicationContext, + R.string.login_success, + Toast.LENGTH_LONG + ).show() + + setResult(LOGIN_OK) + finish() + } + }) + } + + companion object { + const val LOGIN_OK = 0 + const val LOGIN_ERROR = 1 + const val HTTP_CODE_OK = 200 + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/NewBoardActivity.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/NewBoardActivity.java new file mode 100644 index 0000000..887b245 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/NewBoardActivity.java @@ -0,0 +1,69 @@ +package it.unisannio.ding.ids.wedroid.app.view; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.Switch; +import android.widget.Toast; + +import it.unisannio.ding.ids.wedroid.app.R; +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardBackgroundColor; + +public class NewBoardActivity extends AppCompatActivity { + private EditText boardName; + private Switch isPrivate; + private Spinner colorPicker; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_new_board); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + boardName = findViewById(R.id.newBoardName); + isPrivate = findViewById(R.id.newBoardPermission); + + colorPicker = findViewById(R.id.newBoardColor); + ArrayAdapter adapter = ArrayAdapter.createFromResource( + this, + R.array.board_background_colors, + android.R.layout.simple_spinner_item + ); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + colorPicker.setAdapter(adapter); + + } + + public void onDone(View v) { + if (boardName.getText().toString().equals("")) { + Toast.makeText(this, R.string.on_add_new_board_empty_name, Toast.LENGTH_LONG) + .show(); + return; + } + + Intent data = new Intent(); + data.putExtra(BOARD_NAME, boardName.getText().toString()); + data.putExtra(BOARD_PRIVATE, isPrivate.isChecked()); + data.putExtra(BOARD_BACKGROUND_COLOR, colorPicker.getSelectedItem().toString()); + setResult(RESULT_OK, data); + finish(); + } + + public void onCancel(View v) { + finish(); + } + + public static final int RESULT_OK = 17; + public static final String BOARD_NAME = "BOARD_NAME"; + public static final String BOARD_PRIVATE = "BOARD_PRIVATE"; + public static final String BOARD_BACKGROUND_COLOR = "BOARD_BACKGROUND_COLOR"; +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/WListsListActivity.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/WListsListActivity.java new file mode 100644 index 0000000..e42b9f9 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/WListsListActivity.java @@ -0,0 +1,118 @@ +package it.unisannio.ding.ids.wedroid.app.view; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.List; +import java.util.Objects; + +import it.unisannio.ding.ids.wedroid.app.R; + +import it.unisannio.ding.ids.wedroid.app.data.entity.WList; +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper; +import it.unisannio.ding.ids.wedroid.app.view.adapter.WListsAdapter; +import it.unisannio.ding.ids.wedroid.app.viewmodel.WListsListViewModel; + +public class WListsListActivity extends AppCompatActivity { + + Toolbar myToolbar; + int barColor; + WListsListViewModel viewModel; + RecyclerView recyclerView; + //SwipeRefreshLayout swipeRefreshLayout; + SharedPreferenceHelper sp; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_wlists_view); + myToolbar = findViewById(R.id.my_toolbar); + setSupportActionBar(myToolbar); + //swipeRefreshLayout = findViewById(R.id.pullToRefresh); + sp = new SharedPreferenceHelper(this); + + Intent i = getIntent(); + String boardTitle = i.getStringExtra("barTitle"); + Objects.requireNonNull(getSupportActionBar()).setTitle(boardTitle); + barColor = i.getIntExtra("barColor", 0); + setResult(WListsListActivity.RESULT_OK, i); + + myToolbar.setBackgroundColor(barColor); + + recyclerView = findViewById(R.id.recyclerviewWList); + initializeUi(sp.getBoardId()); + + FloatingActionButton fab = findViewById(R.id.synchronize); + fab.setBackgroundTintList(ColorStateList.valueOf(barColor)); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + viewModel.refresh(); + } + }); + } + + private void initializeUi(final String idBoard) { + final WListsAdapter adapter = new WListsAdapter(this); + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(this, + LinearLayoutManager.HORIZONTAL, false)); + //recyclerView.setHasFixedSize(true); + + viewModel = new ViewModelProvider(this).get(WListsListViewModel.class); + + viewModel.getAllWLists().observe(this, new Observer>() { + @Override + public void onChanged(List wLists) { + adapter.setWLists(wLists); + } + }); + + swipeBottomToDelete(idBoard); + + /* BUG REPORT: Refresh of page enter in conflict with swipe down for delete list + * swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + viewModel.refresh(); + } + });*/ + } + + private void swipeBottomToDelete(final String idBoard) { + ItemTouchHelper.SimpleCallback simpleCallback = new ItemTouchHelper.SimpleCallback(0, + ItemTouchHelper.DOWN) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + int pos = viewHolder.getAdapterPosition(); + viewModel.deleteWList(pos, idBoard); + Toast.makeText(getApplicationContext(), "List deleted", Toast.LENGTH_LONG).show(); + + } + }; + new ItemTouchHelper(simpleCallback).attachToRecyclerView(recyclerView); + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt new file mode 100644 index 0000000..b62567f --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt @@ -0,0 +1,56 @@ +package it.unisannio.ding.ids.wedroid.app.view.adapter + +import android.content.Context +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import it.unisannio.ding.ids.wedroid.app.R +import it.unisannio.ding.ids.wedroid.app.data.entity.Board +import it.unisannio.ding.ids.wedroid.app.view.BoardViewActivity + +class BoardsListAdapter internal constructor( + context: Context +) : RecyclerView.Adapter() { + + private val inflater = LayoutInflater.from(context) + private var boards = emptyList() + + inner class BoardViewHolder( + view: View + ) : RecyclerView.ViewHolder(view) { + val boardTitle: TextView = view.findViewById(R.id.boardTitle) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BoardViewHolder { + val view = inflater.inflate(R.layout.board_recycle_item, parent, false) + return BoardViewHolder(view) + } + + override fun getItemCount(): Int { + return boards.size + } + + override fun onBindViewHolder(holder: BoardViewHolder, position: Int) { + val board = boards[position] + holder.boardTitle.text = board.title + + holder.itemView.setOnClickListener { + + val intent = Intent(holder.itemView.context, BoardViewActivity::class.java) + intent.putExtra("idBoard", board.id) + holder.itemView.context.startActivity(intent) + } + } + + internal fun setBoards(boards: List) { + this.boards = boards + notifyDataSetChanged() + } + + companion object { + const val BOARD_ID = "BOARD_ID" + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/WListsAdapter.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/WListsAdapter.java new file mode 100644 index 0000000..dfe929d --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/WListsAdapter.java @@ -0,0 +1,201 @@ +package it.unisannio.ding.ids.wedroid.app.view.adapter; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +import it.unisannio.ding.ids.wedroid.app.R; +import it.unisannio.ding.ids.wedroid.app.data.entity.WList; +import it.unisannio.ding.ids.wedroid.app.util.ServicesFactory; +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper; +import it.unisannio.ding.ids.wedroid.wrapper.entity.Card; +import it.unisannio.ding.ids.wedroid.wrapper.entity.Swimlane; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class WListsAdapter extends RecyclerView.Adapter { + + private final LayoutInflater mInflater; + private List mWLists; // Cached copy of lists + private Context mContext; + private ServicesFactory service; + private SharedPreferenceHelper sp; + + public WListsAdapter(Context context) { + mInflater = LayoutInflater.from(context); + mContext = context; + sp = new SharedPreferenceHelper(mContext); + service = new ServicesFactory(sp); + } + + class WListViewHolder extends RecyclerView.ViewHolder { + private final TextView wListItemView; + ListView listView; + Button button; + + private WListViewHolder(View itemView) { + super(itemView); + wListItemView = itemView.findViewById(R.id.wListTitle); + listView = itemView.findViewById(R.id.listViewCard); + button = itemView.findViewById(R.id.buttonAddCard); + } + } + + @NonNull + @Override + public WListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = mInflater.inflate(R.layout.wlist_recyclerview_item, parent, false); + return new WListViewHolder(itemView); + } + + @SuppressLint("SetTextI18n") + @Override + public void onBindViewHolder(@NonNull final WListViewHolder holder, int position) { + if (mWLists != null) { + final WList current = mWLists.get(position); + final List cardTitle = new ArrayList<>(); + service.getCardService().getAllCards(sp.getBoardId(), current.getId()).enqueue( + new Callback>() { + @Override + public void onResponse(@NotNull Call> call, @NotNull Response> response) { + assert response.body() != null; + for (int i = 0; i < response.body().size(); i++) { + cardTitle.add(response.body().get(i).getTitle()); + } + ArrayAdapter adapter = new ArrayAdapter<>(mContext, + android.R.layout.simple_list_item_1, cardTitle); + holder.listView.setAdapter(adapter); + holder.wListItemView.setText(current.getTitle()); + holder.button.setText("Add Card to " + current.getTitle()); + holder.button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showInputDialog(current.getId()); + } + }); + } + + @Override + public void onFailure(@NotNull Call> call, @NotNull Throwable t) { + } + }); + + /*position of the old content holder**/ + + } else { + // Covers the case of data not being ready yet. + holder.wListItemView.setText("No wList"); + } + } + + @Override + public int getItemCount() { + if (mWLists != null) + return mWLists.size(); + else return 0; + } + + public void setWLists(List wList) { + mWLists = wList; + notifyDataSetChanged(); + } + + private void showInputDialog(final String current) { + LayoutInflater layoutInflater = LayoutInflater.from(mContext); + @SuppressLint("InflateParams") View promptView = layoutInflater.inflate( + R.layout.alert_new_card, null); + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(mContext); + alertDialogBuilder.setView(promptView); + + final EditText editText = promptView.findViewById(R.id.edittext); + // setup a dialog window + alertDialogBuilder.setCancelable(false) + .setPositiveButton("OK", new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int id) { + if (!editText.getText().toString().matches("")) { + service.getSwimlanesService().getAllSwimlanes(sp.getBoardId()) + .enqueue(new Callback>() { + @Override + public void onResponse(@NotNull Call> call, + @NotNull Response> response) { + String idDefaultSwimlane = null; + assert response.body() != null; + for (Swimlane swim : response.body()) { + if (swim.getTitle().equalsIgnoreCase("default")) + idDefaultSwimlane = swim.getTitle(); + } + final Card card = new Card(); + card.setTitle(editText.getText().toString()); + card.setAuthorId(sp.getUserId()); + card.setSwimlaneId(idDefaultSwimlane); + //card.setDescription("new card from app"); + service.getListService().getList(sp.getBoardId(), current).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + + service.getCardService().newCard(sp.getBoardId(), current, card).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + if (response.isSuccessful()) + Toast.makeText(mContext, "card posted", + Toast.LENGTH_LONG).show(); + else + Toast.makeText(mContext, "card doesn't posted", + Toast.LENGTH_LONG).show(); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull Throwable t) { + + } + }); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull Throwable t) { + + } + }); + } + + @Override + public void onFailure(@NotNull Call> call, @NotNull Throwable t) { + + } + }); + } else + Toast.makeText(mContext, "cancel", Toast.LENGTH_LONG).show(); + } + }) + .setNegativeButton("Cancel", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + + // create an alert dialog + AlertDialog alert = alertDialogBuilder.create(); + alert.show(); + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardsListViewModel.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardsListViewModel.java new file mode 100644 index 0000000..0d1a79e --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardsListViewModel.java @@ -0,0 +1,53 @@ +package it.unisannio.ding.ids.wedroid.app.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import java.util.List; + +import it.unisannio.ding.ids.wedroid.app.data.database.BoardDatabase; +import it.unisannio.ding.ids.wedroid.app.data.entity.Board; +import it.unisannio.ding.ids.wedroid.app.data.repository.BoardRepository; +import it.unisannio.ding.ids.wedroid.app.util.PreferenceReader; +import it.unisannio.ding.ids.wedroid.app.util.ServicesFactory; +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper; +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardBackgroundColor; + +public class BoardsListViewModel extends AndroidViewModel { + private BoardRepository repository; + private LiveData> allBoards; + + public BoardsListViewModel(@NonNull Application application) { + super(application); + PreferenceReader reader = new SharedPreferenceHelper(application); + repository = new BoardRepository( + BoardDatabase.getDatabase(application).boardDao(), + ServicesFactory.Companion.getInstance(reader).getBoardService(), + reader + ); + + allBoards = repository.getAllBoards(); + } + + public LiveData> getAllBoards() { + return allBoards; + } + + public void insertBoard(String title, boolean isPrivate, BoardBackgroundColor color) { + repository.insertBoard(title, isPrivate, color); + } + + public void deleteBoard(int position) { + List boards = allBoards.getValue(); + + if (boards != null) + repository.deleteBoard(boards.get(position).getId()); + } + + public void refresh() { + repository.synchronize(); + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/WListsListViewModel.java b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/WListsListViewModel.java new file mode 100644 index 0000000..e5281a5 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/WListsListViewModel.java @@ -0,0 +1,47 @@ +package it.unisannio.ding.ids.wedroid.app.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import java.util.List; + +import it.unisannio.ding.ids.wedroid.app.data.database.WListDatabase; +import it.unisannio.ding.ids.wedroid.app.data.entity.WList; +import it.unisannio.ding.ids.wedroid.app.data.repository.WListRepository; +import it.unisannio.ding.ids.wedroid.app.util.PreferenceReader; +import it.unisannio.ding.ids.wedroid.app.util.ServicesFactory; +import it.unisannio.ding.ids.wedroid.app.util.SharedPreferenceHelper; + + +public class WListsListViewModel extends AndroidViewModel { + private WListRepository wListRepository; + private LiveData> allWLists; + + public WListsListViewModel(@NonNull Application application) { + super(application); + PreferenceReader reader = new SharedPreferenceHelper(application); + wListRepository = new WListRepository( + WListDatabase.getDatabase(application).wListDao(), + ServicesFactory.Companion.getInstance(reader).getListService(), + reader); + allWLists = wListRepository.getAllWLists(); + } + + public LiveData> getAllWLists(){ + return allWLists; + } + + public void deleteWList(int position, String idBoard) { + List wList = allWLists.getValue(); + + if (wList != null) + wListRepository.deleteWList(idBoard, wList.get(position).getId()); + } + + public void refresh() { + wListRepository.synchronize(); + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml new file mode 100644 index 0000000..6b6146e --- /dev/null +++ b/app/src/main/res/drawable/ic_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/rounded_shape.xml b/app/src/main/res/drawable/rounded_shape.xml new file mode 100644 index 0000000..255bc75 --- /dev/null +++ b/app/src/main/res/drawable/rounded_shape.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_board_view.xml b/app/src/main/res/layout/activity_board_view.xml new file mode 100644 index 0000000..04c8d00 --- /dev/null +++ b/app/src/main/res/layout/activity_board_view.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_boards_lists.xml b/app/src/main/res/layout/activity_boards_lists.xml new file mode 100644 index 0000000..7282fa5 --- /dev/null +++ b/app/src/main/res/layout/activity_boards_lists.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..0d4b0a5 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,56 @@ + + + + + + + +