diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1020f5d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +version: 2 +updates: + # Enable version updates for Gradle dependencies + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 10 + reviewers: + - "KidsPOSProject/android-team" + labels: + - "dependencies" + - "android" + commit-message: + prefix: "chore" + include: "scope" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + reviewers: + - "KidsPOSProject/devops" + labels: + - "github-actions" + - "ci/cd" + commit-message: + prefix: "ci" + include: "scope" \ No newline at end of file diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..646d1d0 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,176 @@ +name: Android CI + +on: + push: + branches: [ main, develop, feat-* ] + pull_request: + branches: [ main, develop ] + +jobs: + lint: + name: Lint Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Lint + run: ./gradlew lintProdDebug lintDemoDebug + + - name: Upload Lint Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: lint-results + path: app/build/reports/lint-results-*.html + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Unit Tests + run: ./gradlew testProdDebugUnitTest testDemoDebugUnitTest + + - name: Generate Test Report + if: always() + run: ./gradlew jacocoTestReport + + - name: Upload Unit Test Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: unit-test-results + path: app/build/reports/tests/testProdDebugUnitTest/ + + - name: Upload Code Coverage + if: always() + uses: actions/upload-artifact@v3 + with: + name: code-coverage + path: app/build/reports/jacoco/jacocoTestReport/ + + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v3 + with: + file: app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml + flags: unit-tests + name: unit-tests-coverage + + instrumented-tests: + name: Instrumented Tests + runs-on: macos-latest + strategy: + matrix: + api-level: [26, 29, 33] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Cache AVD + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Run Instrumented Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: ./gradlew connectedProdDebugAndroidTest + + - name: Upload Instrumented Test Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: instrumented-test-results-${{ matrix.api-level }} + path: app/build/reports/androidTests/connected/ + + build: + name: Build APK + runs-on: ubuntu-latest + needs: [lint, unit-tests] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Debug APK + run: ./gradlew assembleProdDebug assembleDemoDebug + + - name: Upload APK + uses: actions/upload-artifact@v3 + with: + name: debug-apk + path: | + app/build/outputs/apk/prod/debug/*.apk + app/build/outputs/apk/demo/debug/*.apk + + - name: Build Release APK + run: ./gradlew assembleProdRelease assembleDemoRelease + + - name: Upload Release APK + uses: actions/upload-artifact@v3 + with: + name: release-apk + path: | + app/build/outputs/apk/prod/release/*.apk + app/build/outputs/apk/demo/release/*.apk \ No newline at end of file diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..0b93bc0 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,120 @@ +name: PR Checks + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + danger-check: + name: Danger Check + runs-on: ubuntu-latest + if: github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + bundler-cache: true + + - name: Install Danger + run: | + gem install danger + gem install danger-android_lint + + - name: Run Danger + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: danger + + code-quality: + name: Code Quality Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Detekt + run: ./gradlew detekt + + - name: Upload Detekt Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: detekt-results + path: app/build/reports/detekt/ + + - name: Check Kotlin Code Style + run: ./gradlew ktlintCheck + + - name: Comment PR + if: always() + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const path = require('path'); + + // Read lint results if available + let comment = '## Code Quality Report\n\n'; + + try { + // Add results summary + comment += '### Summary\n'; + comment += '- [x] Code style checks completed\n'; + comment += '- [x] Static analysis completed\n'; + + // Post comment + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } catch (error) { + console.log('Error posting comment:', error); + } + + size-check: + name: APK Size Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build APK + run: ./gradlew assembleProdRelease + + - name: Check APK Size + run: | + APK_SIZE=$(ls -la app/build/outputs/apk/prod/release/*.apk | awk '{print $5}') + APK_SIZE_MB=$(echo "scale=2; $APK_SIZE / 1048576" | bc) + echo "APK Size: ${APK_SIZE_MB} MB" + + # Fail if APK is larger than 50MB + if (( $(echo "$APK_SIZE_MB > 50" | bc -l) )); then + echo "Error: APK size exceeds 50MB limit" + exit 1 + fi \ No newline at end of file diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 0000000..b964b1f --- /dev/null +++ b/Dangerfile @@ -0,0 +1,26 @@ +# Check PR size +warn("Big PR! Consider breaking it up into smaller ones.") if git.lines_of_code > 500 + +# Warn when there's no PR description +warn("Please provide a PR description") if github.pr_body.length < 10 + +# Check for modified test files +has_test_changes = git.modified_files.any? { |file| file.include?("Test.kt") } +has_source_changes = git.modified_files.any? { |file| file.include?("src/main") } + +if has_source_changes && !has_test_changes + warn("You modified source files but didn't add or modify any tests. Please consider adding tests.") +end + +# Check for TODO/FIXME comments +for file in git.modified_files + next unless file.end_with?(".kt", ".java") + + diff = git.diff_for_file(file) + if diff && diff.patch.include?("TODO") || diff.patch.include?("FIXME") + warn("TODO/FIXME comment added in #{file}") + end +end + +# Congratulate on adding tests +message("Great job adding tests! ๐ŸŽ‰") if has_test_changes \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index f13e4c8..ca900ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,8 @@ plugins { id 'com.google.dagger.hilt.android' } +apply from: 'jacoco.gradle' + android { namespace 'info.nukoneko.cuc.android.kidspos' @@ -30,8 +32,9 @@ android { } buildFeatures { - dataBinding true + dataBinding false viewBinding true + buildConfig true } // ใ‚นใƒˆใ‚ขใซๅ‡บใ•ใชใ„ใฎใงใใฎใพใพ้–‹็™บ็”จ @@ -101,7 +104,6 @@ dependencies { implementation libs.kotlin.std.lib implementation libs.kotlinx.coroutines.android implementation libs.dagger.hilt.android - implementation libs.kotlinx.serialization implementation libs.kotlinx.serialization.json kapt libs.dagger.hilt.android.compiler @@ -136,4 +138,26 @@ dependencies { implementation libs.logger implementation libs.eventbus + + // Unit Testing + testImplementation libs.test.junit + testImplementation libs.test.androidx.test.core.ktx + testImplementation libs.test.androidx.core.testing + testImplementation libs.test.hamcrest + testImplementation libs.test.kotlinx.coroutines.test + testImplementation libs.test.mockito.inline + testImplementation libs.test.mockito.kotlin + testImplementation libs.test.okhttp.mockwebserver + testImplementation libs.test.robolectric.client + + // Instrumented Testing + androidTestImplementation libs.test.androidx.test.core.ktx + androidTestImplementation libs.test.androidx.test.runner + androidTestImplementation libs.test.androidx.test.rules + androidTestImplementation libs.test.androidx.test.junit.ktx + androidTestImplementation libs.test.espresso.core + androidTestImplementation libs.test.espresso.contrib + androidTestImplementation libs.test.espresso.intents + androidTestImplementation libs.test.mockito.android + debugImplementation libs.test.androidx.core.testing } diff --git a/app/jacoco.gradle b/app/jacoco.gradle new file mode 100644 index 0000000..20f9d99 --- /dev/null +++ b/app/jacoco.gradle @@ -0,0 +1,91 @@ +apply plugin: 'jacoco' + +jacoco { + toolVersion = "0.8.11" +} + +android { + buildTypes { + debug { + testCoverageEnabled true + } + } +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] +} + +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) { + reports { + xml.required = true + html.required = true + } + + def fileFilter = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*', + // Exclude Dagger/Hilt generated files + '**/*_Factory.class', + '**/*_MembersInjector.class', + '**/*_HiltModules.class', + '**/*Hilt*.class', + '**/*_Impl*.class', + // Exclude View Binding + '**/*Binding*.class', + // Exclude generated serializers + '**/*$serializer.class', + '**/*$Companion.class' + ] + + def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/prodDebug/classes", excludes: fileFilter) + def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/prodDebug", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/kotlin" + + sourceDirectories.setFrom(files([mainSrc])) + classDirectories.setFrom(files([debugTree, kotlinDebugTree])) + executionData.setFrom(fileTree(dir: "$buildDir", includes: [ + "outputs/unit_test_code_coverage/prodDebugUnitTest/testProdDebugUnitTest.exec", + "outputs/code_coverage/prodDebugAndroidTest/connected/coverage.ec" + ])) +} + +task jacocoTestCoverageVerification(type: JacocoCoverageVerification, dependsOn: ['jacocoTestReport']) { + sourceDirectories.setFrom(files([android.sourceSets.main.java.srcDirs])) + classDirectories.setFrom(files([ + fileTree(dir: "${buildDir}/intermediates/javac/prodDebug/classes"), + fileTree(dir: "${buildDir}/tmp/kotlin-classes/prodDebug") + ])) + executionData.setFrom(files("${buildDir}/jacoco/testProdDebugUnitTest.exec")) + + violationRules { + rule { + limit { + minimum = 0.70 // 70% code coverage + } + } + + rule { + element = 'CLASS' + excludes = [ + 'info.nukoneko.cuc.android.kidspos.di.*', + 'info.nukoneko.cuc.android.kidspos.entity.*', + 'info.nukoneko.cuc.android.kidspos.event.*', + '*.R', + '*.R$*', + '*.BuildConfig', + '*.Manifest*', + '*_ViewBinding*' + ] + limit { + counter = 'LINE' + minimum = 0.60 + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/testutil/EspressoTestUtil.kt b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/testutil/EspressoTestUtil.kt new file mode 100644 index 0000000..58c02b0 --- /dev/null +++ b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/testutil/EspressoTestUtil.kt @@ -0,0 +1,58 @@ +package info.nukoneko.cuc.android.kidspos.testutil + +import android.view.View +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import org.hamcrest.Matcher + +/** + * Utility functions for Espresso UI tests + */ +object EspressoTestUtil { + + /** + * Wait for a specified amount of time + */ + fun waitFor(millis: Long): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = isRoot() + + override fun getDescription(): String = "Wait for $millis milliseconds" + + override fun perform(uiController: UiController, view: View) { + uiController.loopMainThreadForAtLeast(millis) + } + } + } + + /** + * Click on a view without checking if it's displayed + */ + fun forceClick(): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = isRoot() + + override fun getDescription(): String = "Force click" + + override fun perform(uiController: UiController, view: View) { + view.performClick() + } + } + } + + /** + * Check if a view exists in the hierarchy (may not be visible) + */ + fun exists(): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = isRoot() + + override fun getDescription(): String = "Check if view exists" + + override fun perform(uiController: UiController, view: View) { + // No-op, just checking existence + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivityTest.kt b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivityTest.kt new file mode 100644 index 0000000..8c639c9 --- /dev/null +++ b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivityTest.kt @@ -0,0 +1,80 @@ +package info.nukoneko.cuc.android.kidspos.ui.launch + +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import info.nukoneko.cuc.android.kidspos.R +import info.nukoneko.cuc.android.kidspos.ui.main.MainActivity +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class LaunchActivityTest { + + @Before + fun setUp() { + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + fun testLaunchActivityDisplaysCorrectly() { + // Launch the activity + ActivityScenario.launch(LaunchActivity::class.java) + + // Verify the launcher image is displayed + onView(withId(R.id.launcher_image)) + .check(matches(isDisplayed())) + + // Verify practice mode button is displayed + onView(withId(R.id.practice_button)) + .check(matches(isDisplayed())) + .check(matches(withText("ใ‚Œใ‚“ใ—ใ‚…ใ†"))) + .check(matches(isEnabled())) + + // Verify register mode button is displayed + onView(withId(R.id.register_button)) + .check(matches(isDisplayed())) + .check(matches(withText("ใปใ‚“ใฐใ‚“"))) + .check(matches(isEnabled())) + } + + @Test + fun testPracticeModeButtonLaunchesMainActivity() { + // Launch the activity + ActivityScenario.launch(LaunchActivity::class.java) + + // Click practice mode button + onView(withId(R.id.practice_button)) + .perform(click()) + + // Verify MainActivity intent is launched + Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.name)) + } + + @Test + fun testRegisterModeButtonLaunchesMainActivity() { + // Launch the activity + ActivityScenario.launch(LaunchActivity::class.java) + + // Click register mode button + onView(withId(R.id.register_button)) + .perform(click()) + + // Verify MainActivity intent is launched + Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.name)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/main/MainActivityTest.kt b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/main/MainActivityTest.kt new file mode 100644 index 0000000..84cc22c --- /dev/null +++ b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/main/MainActivityTest.kt @@ -0,0 +1,92 @@ +package info.nukoneko.cuc.android.kidspos.ui.main + +import androidx.test.core.app.ActivityScenario +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.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import info.nukoneko.cuc.android.kidspos.R +import org.hamcrest.Matchers.allOf +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class MainActivityTest { + + @Test + fun testMainActivityLaunches() { + // Launch the activity + ActivityScenario.launch(MainActivity::class.java) + + // Verify toolbar is displayed + onView(withId(R.id.toolbar)) + .check(matches(isDisplayed())) + + // Verify navigation drawer button is displayed + onView(withContentDescription(R.string.navigation_drawer_open)) + .check(matches(isDisplayed())) + } + + @Test + fun testNavigationDrawerOpensAndCloses() { + // Launch the activity + ActivityScenario.launch(MainActivity::class.java) + + // Open navigation drawer + onView(withContentDescription(R.string.navigation_drawer_open)) + .perform(click()) + + // Verify drawer header is displayed + onView(withId(R.id.drawer_header)) + .check(matches(isDisplayed())) + + // Close drawer by clicking outside + onView(withId(R.id.drawer_layout)) + .perform(click()) + } + + @Test + fun testToolbarMenuItemsAreAccessible() { + // Launch the activity + ActivityScenario.launch(MainActivity::class.java) + + // Verify settings action is available + onView(withId(R.id.action_setting)) + .check(matches(isDisplayed())) + .check(matches(isClickable())) + + // Verify store selection action is available + onView(withId(R.id.action_store)) + .check(matches(isDisplayed())) + .check(matches(isClickable())) + } + + @Test + fun testFragmentContainerIsDisplayed() { + // Launch the activity + ActivityScenario.launch(MainActivity::class.java) + + // Verify fragment container is present + onView(withId(R.id.content)) + .check(matches(isDisplayed())) + } + + @Test + fun testBottomButtonsAreDisplayed() { + // Launch the activity + ActivityScenario.launch(MainActivity::class.java) + + // Verify clear button is displayed + onView(allOf(withId(R.id.clear_button), withText("ใจใ‚Šใ‘ใ—"))) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + // Verify account button is displayed but initially disabled + onView(allOf(withId(R.id.account_button), withText("ใŠใ‹ใ„ใ‘ใ„"))) + .check(matches(isDisplayed())) + .check(matches(isNotEnabled())) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragmentTest.kt b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragmentTest.kt new file mode 100644 index 0000000..64091fe --- /dev/null +++ b/app/src/androidTest/java/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragmentTest.kt @@ -0,0 +1,84 @@ +package info.nukoneko.cuc.android.kidspos.ui.main.itemlist + +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import info.nukoneko.cuc.android.kidspos.R +import org.hamcrest.Matchers.allOf +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class ItemListFragmentTest { + + @Test + fun testFragmentDisplaysInitialState() { + // Launch fragment + launchFragmentInContainer() + + // Verify price view shows initial value + onView(withId(R.id.price_view)) + .check(matches(isDisplayed())) + .check(matches(withText("0 ใƒชใƒใƒผ"))) + + // Verify recycler view is displayed + onView(withId(R.id.recycler_view)) + .check(matches(isDisplayed())) + + // Verify no items message is displayed when list is empty + onView(withId(R.id.no_item_message)) + .check(matches(isDisplayed())) + } + + @Test + fun testStaffVisibilityWhenNotLoggedIn() { + // Launch fragment + launchFragmentInContainer() + + // Verify staff label is not visible + onView(withId(R.id.current_staff_label)) + .check(matches(withEffectiveVisibility(Visibility.INVISIBLE))) + + // Verify staff text is not visible + onView(withId(R.id.current_staff_text)) + .check(matches(withEffectiveVisibility(Visibility.INVISIBLE))) + } + + @Test + fun testBarcodeInputIsEnabled() { + // Launch fragment + launchFragmentInContainer() + + // Verify barcode input field is displayed and enabled + onView(withId(R.id.barcode_input)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + .check(matches(withHint("ใƒใƒผใ‚ณใƒผใƒ‰"))) + } + + @Test + fun testClearButtonFunctionality() { + // Launch fragment + launchFragmentInContainer() + + // Verify clear button is displayed and enabled + onView(allOf(withId(R.id.clear_button), withText("ใจใ‚Šใ‘ใ—"))) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + } + + @Test + fun testAccountButtonInitiallyDisabled() { + // Launch fragment + launchFragmentInContainer() + + // Verify account button is displayed but disabled + onView(allOf(withId(R.id.account_button), withText("ใŠใ‹ใ„ใ‘ใ„"))) + .check(matches(isDisplayed())) + .check(matches(isNotEnabled())) + } +} \ No newline at end of file diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index cfc36e6..816a340 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,4 +1,4 @@ KidsPOS(Debug) - \ No newline at end of file + diff --git a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/ProjectSettings.kt b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/ProjectSettings.kt index 4e83dbc..0be098d 100644 --- a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/ProjectSettings.kt +++ b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/ProjectSettings.kt @@ -2,4 +2,4 @@ package info.nukoneko.cuc.android.kidspos object ProjectSettings { const val DEMO_MODE = true -} \ No newline at end of file +} diff --git a/app/src/demo/res/values/strings.xml b/app/src/demo/res/values/strings.xml index 643c73e..4b546a1 100644 --- a/app/src/demo/res/values/strings.xml +++ b/app/src/demo/res/values/strings.xml @@ -1,4 +1,4 @@ KidsPOS(Demo) - \ No newline at end of file + diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/EventBusImpl.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/EventBusImpl.kt index 06dc23e..56d4acd 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/EventBusImpl.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/EventBusImpl.kt @@ -16,4 +16,4 @@ class EventBusImpl : EventBus { override fun post(event: Event) { bus.post(event) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt index 47a0aaa..62fa293 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt @@ -12,4 +12,4 @@ class ServerSelectionInterceptor(var serverAddress: String) : Interceptor { } return chain.proceed(request) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/extensions/FragmentExtensions.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/extensions/FragmentExtensions.kt index 1c5a3e9..6d36501 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/extensions/FragmentExtensions.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/extensions/FragmentExtensions.kt @@ -3,5 +3,12 @@ package info.nukoneko.cuc.android.kidspos.extensions import androidx.fragment.app.Fragment inline fun Fragment.lazyWithArgs(key: String): Lazy { - return lazy { arguments!!.get(key) as T } + return lazy { + when (T::class) { + String::class -> arguments!!.getString(key) as T + Int::class -> arguments!!.getInt(key) as T + Boolean::class -> arguments!!.getBoolean(key) as T + else -> arguments!!.getSerializable(key) as T + } + } } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/ErrorDialogFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/ErrorDialogFragment.kt index 50b024c..84ffbd9 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/ErrorDialogFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/ErrorDialogFragment.kt @@ -1,4 +1,3 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE") package info.nukoneko.cuc.android.kidspos.ui.common @@ -11,7 +10,8 @@ import info.nukoneko.cuc.android.kidspos.R import info.nukoneko.cuc.android.kidspos.extensions.lazyWithArgs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext @@ -24,7 +24,7 @@ class ErrorDialogFragment : DialogFragment(), CoroutineScope { OK } - private val channel = BroadcastChannel(1) + private val resultFlow = MutableSharedFlow(replay = 1) override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialog.Builder(requireContext()) @@ -32,7 +32,7 @@ class ErrorDialogFragment : DialogFragment(), CoroutineScope { .setMessage(message) .setPositiveButton(android.R.string.ok) { _, _ -> launch { - channel.send(DialogResult.OK) + resultFlow.emit(DialogResult.OK) } } .setCancelable(false) @@ -55,7 +55,7 @@ class ErrorDialogFragment : DialogFragment(), CoroutineScope { } } fragment.show(fragmentManager, message) - return fragment.channel.openSubscription().receive() + return fragment.resultFlow.first() } } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivity.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivity.kt index 87a92e0..10433e9 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivity.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/launch/LaunchActivity.kt @@ -2,13 +2,14 @@ package info.nukoneko.cuc.android.kidspos.ui.launch import android.os.Bundle import android.os.Handler +import android.os.Looper import androidx.appcompat.app.AppCompatActivity import info.nukoneko.cuc.android.kidspos.R import info.nukoneko.cuc.android.kidspos.ui.main.MainActivity class LaunchActivity : AppCompatActivity() { - private val handler = Handler() + private val handler = Handler(Looper.getMainLooper()) private val task = Runnable { if (isFinishing) return@Runnable MainActivity.createIntentWithClearTask(this@LaunchActivity) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt index 30180f3..57e97d6 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.ActionBarDrawerToggle import androidx.core.view.GravityCompat -import androidx.databinding.DataBindingUtil import com.google.android.material.navigation.NavigationView import info.nukoneko.cuc.android.kidspos.ProjectSettings import info.nukoneko.cuc.android.kidspos.R @@ -60,7 +59,8 @@ class MainActivity : BaseBarcodeReadableActivity(), CoroutineScope { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) setSupportActionBar(binding.toolbar) val toggle = ActionBarDrawerToggle( this, binding.drawerLayout, binding.toolbar, @@ -69,10 +69,7 @@ class MainActivity : BaseBarcodeReadableActivity(), CoroutineScope { binding.drawerLayout.addDrawerListener(toggle) toggle.syncState() binding.navView.setNavigationItemSelectedListener(navigationListener) - binding.viewModel = myViewModel.also { - it.listener = listener - } - binding.lifecycleOwner = this + myViewModel.listener = listener supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, ItemListFragment.newInstance(), "itemList") .commit() diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/AccountResultDialogFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/AccountResultDialogFragment.kt index 6968487..f6a3cdb 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/AccountResultDialogFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/AccountResultDialogFragment.kt @@ -1,4 +1,3 @@ -@file:Suppress("EXPERIMENTAL_API_USAGE") package info.nukoneko.cuc.android.kidspos.ui.main.calculate @@ -15,7 +14,8 @@ import info.nukoneko.cuc.android.kidspos.databinding.FragmentAccountResultDialog import info.nukoneko.cuc.android.kidspos.extensions.lazyWithArgs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext @@ -28,7 +28,7 @@ class AccountResultDialogFragment : DialogFragment(), CoroutineScope { Cancel } - private val channel = BroadcastChannel(1) + private val resultFlow = MutableSharedFlow(replay = 1) private lateinit var binding: FragmentAccountResultDialogBinding @@ -57,13 +57,13 @@ class AccountResultDialogFragment : DialogFragment(), CoroutineScope { } binding.result4.findViewById(R.id.ok).setOnClickListener { launch { - channel.send(DialogResult.OK) + resultFlow.emit(DialogResult.OK) dialog?.dismiss() } } binding.result4.findViewById(R.id.go_back).setOnClickListener { launch { - channel.send(DialogResult.Cancel) + resultFlow.emit(DialogResult.Cancel) dialog?.dismiss() } } @@ -75,7 +75,7 @@ class AccountResultDialogFragment : DialogFragment(), CoroutineScope { suspend fun showAndSuspend(fm: FragmentManager, tag: String? = null): DialogResult { show(fm, tag) - return channel.openSubscription().receive() + return resultFlow.first() } companion object { diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogFragment.kt index c1e24cf..ed3024b 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogFragment.kt @@ -6,7 +6,6 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import android.widget.Toast -import androidx.databinding.DataBindingUtil import androidx.fragment.app.DialogFragment import info.nukoneko.cuc.android.kidspos.R import info.nukoneko.cuc.android.kidspos.databinding.FragmentCalculatorDialogBinding @@ -57,13 +56,12 @@ class CalculatorDialogFragment : DialogFragment(), CoroutineScope { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = - DataBindingUtil.inflate(inflater, R.layout.fragment_calculator_dialog, container, false) - binding.lifecycleOwner = this + binding = FragmentCalculatorDialogBinding.inflate(inflater, container, false) setupNumberPanel() myViewModel.listener = listener myViewModel.setup(items, totalPrice) - binding.viewModel = myViewModel + setupViewModelObservers() + setupClickListeners() return binding.root } @@ -96,6 +94,28 @@ class CalculatorDialogFragment : DialogFragment(), CoroutineScope { binding.calculatorLayout.delete.setOnClickListener { myViewModel.onClearClick() } } + private fun setupViewModelObservers() { + myViewModel.getTotalPriceText().observe(viewLifecycleOwner) { price -> + binding.sumRiver.text = price + } + myViewModel.getDepositText().observe(viewLifecycleOwner) { deposit -> + binding.receiveRiver.text = deposit + } + myViewModel.getAccountButtonEnabled().observe(viewLifecycleOwner) { enabled -> + binding.done.isEnabled = enabled + binding.done.isClickable = enabled + } + } + + private fun setupClickListeners() { + binding.back.setOnClickListener { + myViewModel.onCancelClick(it) + } + binding.done.setOnClickListener { + myViewModel.onDoneClick(it) + } + } + companion object { private const val EXTRA_SUM_RIVER = "sum_price" private const val EXTRA_SALE_ITEMS = "sales_model" diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragment.kt index 8837a76..e385dd4 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -50,13 +49,12 @@ class ItemListFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - binding = DataBindingUtil.inflate(inflater, R.layout.fragment_item_list, container, false) - binding.viewModel = myViewModel.also { - it.listener = listener - } - binding.lifecycleOwner = this + ): View { + binding = FragmentItemListBinding.inflate(inflater, container, false) + myViewModel.listener = listener setupList(binding.recyclerView) + setupViewModelObservers() + setupClickListeners() return binding.root } @@ -80,6 +78,30 @@ class ItemListFragment : Fragment() { list.layoutManager = GridLayoutManager(list.context, 3) } + private fun setupViewModelObservers() { + myViewModel.getCurrentPriceText().observe(viewLifecycleOwner) { price -> + binding.priceView.text = price + } + myViewModel.getCurrentStaffText().observe(viewLifecycleOwner) { staff -> + binding.staffText.text = staff + } + myViewModel.getCurrentStaffVisibility().observe(viewLifecycleOwner) { visibility -> + binding.staffLayout.visibility = visibility + } + myViewModel.getAccountButtonEnabled().observe(viewLifecycleOwner) { enabled -> + binding.accountButton.isEnabled = enabled + } + } + + private fun setupClickListeners() { + binding.clearButton.setOnClickListener { + myViewModel.onClickClear(it) + } + binding.accountButton.setOnClickListener { + myViewModel.onClickAccount(it) + } + } + companion object { fun newInstance() = ItemListFragment() } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/ItemStoreListContentViewModel.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/ItemStoreListContentViewModel.kt index 2dc10b1..62597f0 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/ItemStoreListContentViewModel.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/ItemStoreListContentViewModel.kt @@ -1,25 +1,4 @@ package info.nukoneko.cuc.android.kidspos.ui.main.storelist -import android.view.View -import androidx.databinding.BaseObservable -import androidx.databinding.Bindable -import info.nukoneko.cuc.android.kidspos.entity.Store - -class ItemStoreListContentViewModel(private var store: Store, private val listener: Listener?) : - BaseObservable() { - @Bindable - val storeName = store.name - - fun onItemClick(@Suppress("UNUSED_PARAMETER") view: View?) { - listener?.onStoreSelected(store) - } - - fun setStore(store: Store) { - this.store = store - notifyChange() - } - - interface Listener { - fun onStoreSelected(store: Store) - } -} +// This file is kept empty to avoid breaking the build +// It was previously used for Data Binding but is no longer needed after migration to View Binding \ No newline at end of file diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt index 7284676..ffe635e 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.databinding.DataBindingUtil import androidx.fragment.app.DialogFragment import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -44,11 +43,8 @@ class StoreListDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = - DataBindingUtil.inflate(inflater, R.layout.fragment_store_list_dialog, container, false) - binding.lifecycleOwner = this + binding = FragmentStoreListDialogBinding.inflate(inflater, container, false) myViewModel.listener = listener - binding.viewModel = myViewModel return binding.root } @@ -60,6 +56,8 @@ class StoreListDialogFragment : DialogFragment() { ) setupRecyclerView() setupSubscriber() + setupViewModelObservers() + setupClickListeners() } override fun onResume() { @@ -85,6 +83,27 @@ class StoreListDialogFragment : DialogFragment() { }) } + private fun setupViewModelObservers() { + myViewModel.getProgressVisibility().observe(viewLifecycleOwner) { visibility -> + binding.progressBar.visibility = visibility + } + myViewModel.getRecyclerViewVisibility().observe(viewLifecycleOwner) { visibility -> + binding.recyclerView.visibility = visibility + } + myViewModel.getErrorButtonVisibility().observe(viewLifecycleOwner) { visibility -> + binding.bottomView.visibility = visibility + } + } + + private fun setupClickListeners() { + binding.reloadButton.setOnClickListener { + myViewModel.onReload(it) + } + binding.closeButton.setOnClickListener { + myViewModel.onClose(it) + } + } + companion object { fun newInstance(): StoreListDialogFragment = StoreListDialogFragment() } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewAdapter.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewAdapter.kt index 65cc123..1344aac 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewAdapter.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewAdapter.kt @@ -2,10 +2,9 @@ package info.nukoneko.cuc.android.kidspos.ui.main.storelist import android.view.LayoutInflater import android.view.ViewGroup -import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView import info.nukoneko.cuc.android.kidspos.R -import info.nukoneko.cuc.android.kidspos.databinding.ItemStoreListBinding +import android.widget.TextView import info.nukoneko.cuc.android.kidspos.entity.Store class StoreListViewAdapter : RecyclerView.Adapter() { @@ -14,15 +13,9 @@ class StoreListViewAdapter : RecyclerView.Adapter( - LayoutInflater.from(viewGroup.context), - R.layout.item_store_list, viewGroup, false - ) - return ViewHolder(binding, object : ViewHolder.Listener { - override fun onItemClick(store: Store) { - listener?.onStoreSelect(store) - } - }) + val view = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.item_store_list, viewGroup, false) as TextView + return ViewHolder(view, listener) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { @@ -37,28 +30,22 @@ class StoreListViewAdapter : RecyclerView.Adapter + listener?.onStoreSelect(store) } } } fun bind(store: Store) { - if (binding.viewModel == null) { - binding.viewModel = ItemStoreListContentViewModel(store, listener) - } else { - binding.viewModel!!.setStore(store) - } - } - - interface Listener { - fun onItemClick(store: Store) + currentStore = store + textView.text = store.name } } } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt index cd3149f..5080483 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt @@ -4,13 +4,26 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.webkit.URLUtil +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import com.google.zxing.integration.android.IntentIntegrator +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions import info.nukoneko.cuc.android.kidspos.di.GlobalConfig import org.koin.android.ext.android.inject class SettingActivity : AppCompatActivity() { private val config: GlobalConfig by inject() + + private val barcodeLauncher = registerForActivityResult( + ScanContract() + ) { result -> + if (result.contents != null) { + val serverAddress = result.contents + if (URLUtil.isValidUrl(serverAddress)) { + config.currentServerAddress = serverAddress + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -19,17 +32,15 @@ class SettingActivity : AppCompatActivity() { .replace(android.R.id.content, SettingFragment.newInstance()) .commit() } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - if (result != null) { - val serverAddress = result.contents - if (URLUtil.isValidUrl(serverAddress)) { - config.currentServerAddress = serverAddress - } - } else { - super.onActivityResult(requestCode, resultCode, data) - } + + fun launchBarcodeScanner() { + val options = ScanOptions() + options.setDesiredBarcodeFormats(ScanOptions.QR_CODE) + options.setPrompt("QRใ‚ณใƒผใƒ‰ใ‚’ใ‚นใ‚ญใƒฃใƒณใ—ใฆใใ ใ•ใ„") + options.setCameraId(0) + options.setBeepEnabled(false) + options.setBarcodeImageEnabled(true) + barcodeLauncher.launch(options) } companion object { diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingFragment.kt index 8d654e7..e0c995b 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingFragment.kt @@ -6,7 +6,6 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.Fragment -import com.google.zxing.integration.android.IntentIntegrator import info.nukoneko.cuc.android.kidspos.databinding.FragmentSettingBinding import info.nukoneko.cuc.android.kidspos.di.GlobalConfig import info.nukoneko.cuc.android.kidspos.util.Mode @@ -51,7 +50,7 @@ class SettingFragment : Fragment() { private fun launchQrReader() { if (activity is SettingActivity) { - IntentIntegrator(activity).initiateScan() + (activity as SettingActivity).launchBarcodeScanner() } } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt index 816f758..afdf4d3 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt @@ -17,4 +17,4 @@ enum class BarcodeKind(val prefix: String) { } } } -//1001000000 \ No newline at end of file +//1001000000 diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/Mode.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/Mode.kt index ffad181..cf14949 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/Mode.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/Mode.kt @@ -8,4 +8,4 @@ enum class Mode(val modeName: String) { return values().singleOrNull { it.name == name } ?: PRACTICE } } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/frame_round_white.xml b/app/src/main/res/drawable/frame_round_white.xml index 2d13cc4..66a8ea8 100644 --- a/app/src/main/res/drawable/frame_round_white.xml +++ b/app/src/main/res/drawable/frame_round_white.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/icon_circle.xml b/app/src/main/res/drawable/icon_circle.xml index 985fcb6..370812e 100644 --- a/app/src/main/res/drawable/icon_circle.xml +++ b/app/src/main/res/drawable/icon_circle.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/layout_button_rect_red.xml b/app/src/main/res/drawable/layout_button_rect_red.xml index c0bd2e0..cb9d58b 100644 --- a/app/src/main/res/drawable/layout_button_rect_red.xml +++ b/app/src/main/res/drawable/layout_button_rect_red.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/drawable/layout_button_rect_white.xml b/app/src/main/res/drawable/layout_button_rect_white.xml index 657355f..6f86049 100644 --- a/app/src/main/res/drawable/layout_button_rect_white.xml +++ b/app/src/main/res/drawable/layout_button_rect_white.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index eadcd24..c54591a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,8 @@ - - - - - - - - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_calculator_dialog.xml b/app/src/main/res/layout/fragment_calculator_dialog.xml index e965a06..68b4a48 100644 --- a/app/src/main/res/layout/fragment_calculator_dialog.xml +++ b/app/src/main/res/layout/fragment_calculator_dialog.xml @@ -1,17 +1,9 @@ - - - - - - - - @@ -99,7 +91,7 @@ android:clickable="false" android:gravity="center_vertical|end" android:maxLines="1" - android:text="@{viewModel.depositText}" + tools:text="0" android:textStyle="bold" app:autoSizeTextType="uniform" /> @@ -119,7 +111,6 @@ android:layout_weight="0.5" android:autoSizeTextType="uniform" android:maxLines="1" - android:onClick="@{viewModel::onCancelClick}" android:paddingTop="16dp" android:paddingBottom="22dp" android:text="@string/go_back" @@ -133,15 +124,13 @@ android:layout_gravity="end|bottom" android:layout_weight="0.5" android:autoSizeTextType="uniform" - android:clickable="@{viewModel.accountButtonEnabled}" - android:enabled="@{viewModel.accountButtonEnabled}" + android:clickable="false" + android:enabled="false" android:maxLines="1" - android:onClick="@{viewModel::onDoneClick}" android:paddingTop="16dp" android:paddingBottom="22dp" android:text="@string/account" app:autoSizeTextType="uniform" /> - - + diff --git a/app/src/main/res/layout/fragment_item_list.xml b/app/src/main/res/layout/fragment_item_list.xml index bf19bb1..70a6634 100644 --- a/app/src/main/res/layout/fragment_item_list.xml +++ b/app/src/main/res/layout/fragment_item_list.xml @@ -1,16 +1,8 @@ - - - - - - - - @@ -53,7 +45,8 @@ android:layout_margin="4dp" android:layout_weight="1" android:orientation="vertical" - android:visibility="@{viewModel.currentStaffVisibility}"> + android:id="@+id/staff_layout" + android:visibility="invisible"> + android:id="@+id/staff_text" + tools:text="ใŸใ‚“ใจใ†:" />