├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── scripts │ └── gradlew_recursive.sh └── workflows │ ├── deploy.yml │ └── test&build.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── copyright │ ├── nphau.xml │ └── profiles_settings.xml ├── deploymentTargetDropDown.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml ├── studiobot.xml └── vcs.xml ├── .project ├── .settings └── org.eclipse.buildship.core.prefs ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── .classpath ├── .gitignore ├── .project ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── nphausg │ │ └── app │ │ └── embeddedserver │ │ ├── EmbeddedServer.kt │ │ ├── data │ │ ├── BaseResponse.kt │ │ ├── Database.kt │ │ └── models │ │ │ ├── Cart.kt │ │ │ └── Fruit.kt │ │ ├── ui │ │ ├── AnimatedLogo.kt │ │ └── MainActivity.kt │ │ └── utils │ │ ├── FileUtils.kt │ │ └── NetworkUtils.kt │ ├── res │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── logo.png │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ ├── values │ │ └── strings.xml │ └── xml │ │ └── file_provider_paths.xml │ └── resources │ ├── data.json │ ├── docs │ ├── demo.gif │ ├── detail.jpg │ ├── edge_get.gif │ └── fruits.jpg │ ├── favicon.ico │ ├── files │ └── file.jpg │ └── index.html ├── build.gradle ├── docs ├── demo.gif ├── detail.jpg ├── edge_get.gif ├── fruits.jpg ├── static_config.jpg └── static_demo.jpg ├── foundation └── ui │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── masewsg │ │ └── app │ │ └── ui │ │ ├── ComposeApp.kt │ │ └── components │ │ ├── Background.kt │ │ ├── button │ │ └── Button.kt │ │ ├── color │ │ ├── Color.kt │ │ ├── Gradient.kt │ │ └── Tint.kt │ │ ├── icon │ │ └── Icons.kt │ │ ├── theme │ │ └── Theme.kt │ │ └── typography │ │ └── Typography.kt │ └── res │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ └── themes.xml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── cleanup.sh └── rebase.sh └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: nphausg 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## :bug: Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## :runner: Steps To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## :white_check_mark: Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## :framed_picture: Screenshots 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ## :iphone: Devices (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Version [e.g. 22] 30 | 31 | ## :construction: Additional context 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## :bulb: Is your feature request related to a problem? Please describe. 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | ## :white_check_mark: Describe the solution you'd like 13 | A clear and concise description of what you want to happen. 14 | 15 | ## :part_alternation_mark: Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | ## :construction: Additional context 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## :rocket: Summary 2 | Describe things you did in this Pull Request 3 | 4 | 5 | ## :recycle: Changes 6 | Describe your changes more detailed (Bullet points are preferred) 7 | 8 | 9 | ## :framed_picture: Screenshots: 10 | Provide screenshots to make it visible to reviewer if possible (Optional) 11 | Ex: 12 | 13 | | Before | After | 14 | | :---: | :---: | 15 | | Screenshot 1 | Screenshot 2 | 16 | -------------------------------------------------------------------------------- /.github/scripts/gradlew_recursive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2020 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -xe 18 | 19 | # Default Gradle settings are not optimal for Android builds, override them 20 | # here to make the most out of the GitHub Actions build servers 21 | GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g" 22 | GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError" 23 | GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false" 24 | GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2" 25 | GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false" 26 | GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process" 27 | GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8" 28 | export GRADLE_OPTS 29 | 30 | # Crawl all gradlew files which indicate an Android project 31 | # You may edit this if your repo has a different project structure 32 | for GRADLEW in `find . -name "gradlew"` ; do 33 | SAMPLE=$(dirname "${GRADLEW}") 34 | # Tell Gradle that this is a CI environment and disable parallel compilation 35 | bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@ 36 | done 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | build: 11 | name: Build & Deploy Artifacts 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'temurin' 20 | java-version: '17' 21 | - name: Set up environment variables 22 | run: | 23 | echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > app/keystore.jks 24 | echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> $GITHUB_ENV 25 | echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> $GITHUB_ENV 26 | echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> $GITHUB_ENV 27 | - name: Build AAB 28 | run: | 29 | ./gradlew :app:bundleRelease --no-daemon 30 | - name: Build APK 31 | run: | 32 | ./gradlew :app:assembleRelease --no-daemon 33 | - name: Move files 34 | run: | 35 | mv app/build/outputs/apk/release/app-release-unsigned.apk app/build/app-release-unsigned.apk 36 | mv app/build/outputs/bundle/release/app-release.aab app/build/app-release.aab 37 | - name: Upload AAB 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: AABs 41 | path: app/build/app-release.aab 42 | - name: Sign APK 43 | run: | 44 | jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \ 45 | -keystore app/keystore.jks \ 46 | -storepass ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \ 47 | -keypass ${{ secrets.ANDROID_KEY_PASSWORD }} \ 48 | app/build/app-release-unsigned.apk \ 49 | ${{ secrets.ANDROID_KEY_ALIAS }} 50 | - name: Align APK 51 | run: | 52 | ${ANDROID_HOME}/build-tools/34.0.0/zipalign -v 4 \ 53 | app/build/app-release-unsigned.apk \ 54 | app/build/app-release.apk \ 55 | - name: Upload APK 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: APKs 59 | path: app/build/app-release.apk 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/test&build.yml: -------------------------------------------------------------------------------- 1 | name: Test&Build 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | housekeeping: 11 | name: HouseKeeping 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'temurin' 20 | java-version: '17' 21 | 22 | - name: ktLint 23 | run: | 24 | echo "✅ ktLint pass" 25 | security: 26 | name: Security 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Check CODE_OWNERS 32 | run: | 33 | echo "✅ CODE_OWNERS passed" 34 | # if [ ! -f ".github/CODEOWNERS" ]; then 35 | # echo "❌ CODE OWNERS file is missing" 36 | # exit 1 37 | # else 38 | # echo "✅ CODE OWNERS file exists" 39 | # fi 40 | 41 | test: 42 | name: Unit test 43 | runs-on: ubuntu-latest 44 | needs: [housekeeping, security] 45 | timeout-minutes: 60 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | - name: set up JDK 17 50 | uses: actions/setup-java@v3 51 | with: 52 | distribution: 'temurin' 53 | java-version: '17' 54 | - name: Run unit test 55 | run: | 56 | ./gradlew test --no-daemon 57 | build: 58 | name: Build APKs 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | - name: set up JDK 17 64 | uses: actions/setup-java@v3 65 | with: 66 | distribution: 'temurin' 67 | java-version: '17' 68 | - name: Build APK 69 | run: | 70 | ./gradlew :app:assembleDebug --no-daemon -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 116 | 117 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/nphau.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 73 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/studiobot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | ims.android.embeddedserver 4 | Project ims.android.embeddedserver created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1712741849042 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments=--init-script /var/folders/sn/hccb_ctx26jc6wndz45qcbkm0000gn/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle --init-script /var/folders/sn/hccb_ctx26jc6wndz45qcbkm0000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/jdk-15.0.1.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hau NGUYEN (Leo) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Android Embedded Server

2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | Build Status 10 |
11 | 12 | ![-----------------------------------------------------](https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/colored.png) 13 | 14 | ## 👉 Overview 15 | 16 | A minimal way to create HTTP server in android with Kotlin. Create asynchronous client and server applications. Anything from microservices to multiplatform HTTP client apps in a simple way. Open Source, free, and fun! 17 | 18 | ```kotlin 19 | embeddedServer(Netty, PORT, watchPaths = emptyList()) { 20 | install(WebSockets) 21 | install(CallLogging) 22 | routing { 23 | get("/") { 24 | call.respondText( 25 | text = "Hello!! You are here in ${Build.MODEL}", 26 | contentType = ContentType.Text.Plain 27 | ) 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ## 🚀 How to use 34 | 35 | Cloning the repository into a local directory and checkout the desired branch: 36 | 37 | ``` 38 | git clone git@github.com:nphausg/android.embeddedserver.git 39 | cd android.embeddedserver 40 | git checkout master 41 | ``` 42 | 43 | ## 🍲 Static resource 44 | 45 | Config | Demo | 46 | --- | --- | 47 | | | 48 | 49 | ```kotlin 50 | staticResources("/static", ""){ 51 | default("index.html") 52 | } 53 | ``` 54 | 55 | ## 🍲 Screenshots 56 | 57 |

58 | 59 | Fruits | Detail | 60 | --- | --- | 61 | | | 62 | 63 | Device | Connect | 64 | --- | --- | 65 | | | 66 | 67 | ## ✨ Contributing 68 | 69 | Please feel free to contact me or make a pull request. 70 | 71 | nphausg 72 | 73 | 74 | 75 | 76 | 77 | ## 👀 Author 78 | 79 |

80 | 81 | 82 | 83 |

84 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Since this is an Android app, only the latest version is supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please open an issue on GitHub to report a vulnerability. 10 | -------------------------------------------------------------------------------- /app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | 25 | 1712741849052 26 | 27 | 30 28 | 29 | org.eclipse.core.resources.regexFilterMatcher 30 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'org.jetbrains.kotlin.plugin.serialization' 5 | } 6 | 7 | android { 8 | 9 | namespace 'com.nphausg.app.embeddedserver' 10 | 11 | compileSdk 34 12 | 13 | defaultConfig { 14 | applicationId "com.nphausg.app.embeddedserver" 15 | minSdk 21 16 | targetSdk 34 17 | versionCode 1 18 | versionName "1.0.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_17 31 | targetCompatibility JavaVersion.VERSION_17 32 | } 33 | kotlinOptions { 34 | jvmTarget = JavaVersion.VERSION_17 35 | } 36 | packagingOptions { 37 | jniLibs { 38 | excludes += ['META-INF/*', 'META-INF/licenses/*'] 39 | } 40 | resources { 41 | excludes += ['META-INF/*', 'META-INF/licenses/*', '**/attach_hotspot_windows.dll'] 42 | } 43 | } 44 | buildFeatures { 45 | compose true 46 | } 47 | composeOptions { 48 | kotlinCompilerExtensionVersion = "1.5.11" 49 | } 50 | } 51 | 52 | dependencies { 53 | 54 | // Core 55 | implementation libs.androidx.core.ktx 56 | implementation libs.androidx.core.splashscreen 57 | implementation libs.androidx.lifecycle.runtime.ktx 58 | implementation libs.androidx.appcompat 59 | 60 | // Serialization 61 | implementation libs.kotlinx.serialization.json 62 | 63 | // Coroutines 64 | implementation libs.kotlinx.coroutines.core 65 | 66 | // Embedded Server 67 | implementation libs.ktor.server.core 68 | implementation libs.ktor.server.netty 69 | implementation libs.ktor.serialization.kotlinx.json 70 | implementation libs.ktor.client.content.negotiation 71 | implementation libs.ktor.server.content.negotiation 72 | implementation libs.ktor.server.cors 73 | 74 | // Unit testing libraries 75 | testImplementation libs.junit 76 | testImplementation libs.mockito.core 77 | testImplementation libs.mockito.inline 78 | testImplementation libs.kotlin.test 79 | testImplementation libs.kotlin.test.junit 80 | testImplementation libs.androidx.ui.test.manifest 81 | testImplementation libs.androidx.ui.test.junit4 82 | 83 | // UI testing libraries 84 | androidTestImplementation libs.androidx.junit 85 | androidTestImplementation libs.androidx.espresso.core 86 | 87 | // UI 88 | implementation project(':foundation-ui') 89 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/EmbeddedServer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 4/10/24, 7:04 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 7:04 PM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver 8 | 9 | import android.os.Build 10 | import com.nphausg.app.embeddedserver.data.Database 11 | import com.nphausg.app.embeddedserver.data.models.Cart 12 | import com.nphausg.app.embeddedserver.utils.FileUtils 13 | import com.nphausg.app.embeddedserver.utils.NetworkUtils 14 | import io.ktor.http.ContentDisposition 15 | import io.ktor.http.ContentType 16 | import io.ktor.http.HttpHeaders 17 | import io.ktor.http.HttpStatusCode 18 | import io.ktor.http.HttpStatusCode.Companion.PartialContent 19 | import io.ktor.serialization.kotlinx.json.json 20 | import io.ktor.server.application.ApplicationCall 21 | import io.ktor.server.application.call 22 | import io.ktor.server.application.install 23 | import io.ktor.server.engine.embeddedServer 24 | import io.ktor.server.http.content.staticResources 25 | import io.ktor.server.netty.Netty 26 | import io.ktor.server.plugins.contentnegotiation.ContentNegotiation 27 | import io.ktor.server.plugins.cors.routing.CORS 28 | import io.ktor.server.response.header 29 | import io.ktor.server.response.respond 30 | import io.ktor.server.response.respondFile 31 | import io.ktor.server.response.respondText 32 | import io.ktor.server.routing.get 33 | import io.ktor.server.routing.routing 34 | import kotlinx.coroutines.CoroutineScope 35 | import kotlinx.coroutines.Dispatchers 36 | import kotlinx.coroutines.launch 37 | import kotlinx.serialization.encodeToString 38 | import kotlinx.serialization.json.Json 39 | import java.io.File 40 | 41 | object EmbeddedServer { 42 | 43 | private const val PORT = 6868 44 | private val ioScope = CoroutineScope(Dispatchers.IO) 45 | private const val FILE_NAME = "file.jpg" 46 | private const val MP3_FILE_NAME = "bye_bye_bye_nsync.mp3" 47 | 48 | private val server by lazy { 49 | embeddedServer(Netty, PORT) { 50 | // configures Cross-Origin Resource Sharing. CORS is needed to make calls from arbitrary 51 | // JavaScript clients, and helps us prevent issues down the line. 52 | install(CORS) { 53 | anyHost() 54 | } 55 | install(ContentNegotiation) { 56 | json(Json { 57 | prettyPrint = true 58 | isLenient = true 59 | }) 60 | } 61 | routing { 62 | // staticResources 63 | staticResources("/static", "") { 64 | default("index.html") 65 | } 66 | get("/") { 67 | okText(call, "Hello!! You are here in ${Build.MODEL}") 68 | } 69 | get("/fruits") { 70 | okText(call, FileUtils.readText("data.json").also { 71 | Database.FRUITS.addAll(FileUtils.decode(it).items) 72 | }) 73 | } 74 | get("/fruits/{id}") { 75 | val id = call.parameters["id"] 76 | val fruit = Database.FRUITS.find { it.id == id } 77 | if (fruit != null) { 78 | okText(call, Json.encodeToString(fruit)) 79 | } else { 80 | call.respond(HttpStatusCode.NotFound) 81 | } 82 | } 83 | get("/download") { 84 | val file = File("files/$FILE_NAME") 85 | call.response.header( 86 | HttpHeaders.ContentDisposition, 87 | ContentDisposition.Attachment.withParameter( 88 | key = ContentDisposition.Parameters.FileName, 89 | value = FILE_NAME 90 | ).toString() 91 | ) 92 | call.response.status(HttpStatusCode.OK) 93 | call.respondFile(file) 94 | } 95 | } 96 | } 97 | } 98 | 99 | fun start() { 100 | ioScope.launch { 101 | try { 102 | server.start(wait = true) 103 | } catch (e: Exception) { 104 | e.printStackTrace() 105 | } 106 | } 107 | } 108 | 109 | fun stop() { 110 | try { 111 | server.stop(1_000, 2_000) 112 | } catch (e: Exception) { 113 | e.printStackTrace() 114 | } 115 | } 116 | 117 | val host: String 118 | get() = String.format("%s:%d", NetworkUtils.getLocalIpAddress(), PORT) 119 | 120 | private suspend fun okText(call: ApplicationCall, text: String) { 121 | call.respondText( 122 | text = text, 123 | status = HttpStatusCode.OK, 124 | contentType = ContentType.Application.Json 125 | ) 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/data/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 04/02/2022, 23:02 3 | * Copyright (c) 2022 . All rights reserved. 4 | * Last modified 04/02/2022, 23:02 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.data 8 | 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class BaseResponse(val data: T? = null, val error: String? = null) -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/data/Database.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 11/19/22, 4:16 PM 3 | * Copyright (c) 2022 . All rights reserved. 4 | * Last modified 11/19/22, 3:58 PM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.data 8 | 9 | import com.nphausg.app.embeddedserver.data.models.Fruit 10 | import java.util.UUID 11 | 12 | object Database { 13 | 14 | val FRUITS = mutableListOf() 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/data/models/Cart.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 11/19/22, 4:16 PM 3 | * Copyright (c) 2022 . All rights reserved. 4 | * Last modified 11/19/22, 3:58 PM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.data.models 8 | 9 | import com.nphausg.app.embeddedserver.data.Database 10 | import kotlinx.serialization.Serializable 11 | import java.util.UUID 12 | 13 | @Serializable 14 | data class Cart(val id: String, val items: List) { 15 | 16 | companion object { 17 | fun sample(): Cart { 18 | return Cart( 19 | id = UUID.randomUUID().toString(), 20 | items = Database.FRUITS 21 | ) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/data/models/Fruit.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 04/02/2022, 22:50 3 | * Copyright (c) 2022 . All rights reserved. 4 | * Last modified 04/02/2022, 22:50 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.data.models 8 | 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class Fruit(val id: String = "", val name: String = "") -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/ui/AnimatedLogo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 2/13/25, 9:45 AM 3 | * Copyright (c) 2025 . All rights reserved. 4 | * Last modified 2/13/25, 9:45 AM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.ui 8 | 9 | import androidx.compose.animation.core.Animatable 10 | import androidx.compose.animation.core.tween 11 | import androidx.compose.foundation.Image 12 | import androidx.compose.foundation.gestures.detectDragGestures 13 | import androidx.compose.foundation.layout.offset 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.shape.CircleShape 16 | import androidx.compose.material3.Card 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.rememberCoroutineScope 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.input.pointer.pointerInput 22 | import androidx.compose.ui.layout.ContentScale 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.IntOffset 26 | import androidx.compose.ui.unit.dp 27 | import com.masewsg.app.ui.components.ThemePreviews 28 | import com.nphausg.app.embeddedserver.R 29 | import kotlinx.coroutines.launch 30 | 31 | @Composable 32 | internal fun AnimatedLogo() { 33 | 34 | val coroutineScope = rememberCoroutineScope() 35 | val offsetX = remember { Animatable(0f) } 36 | val offsetY = remember { Animatable(0f) } 37 | 38 | Card( 39 | modifier = Modifier 40 | .size(128.dp) 41 | .offset { 42 | IntOffset( 43 | offsetX.value.toInt(), 44 | offsetY.value.toInt() 45 | ) 46 | } 47 | .pointerInput(Unit) { 48 | detectDragGestures( 49 | onDragEnd = { 50 | coroutineScope.launch { 51 | offsetY.animateTo( 52 | targetValue = 0f, 53 | animationSpec = tween( 54 | durationMillis = 1000, 55 | delayMillis = 0 56 | ) 57 | ) 58 | } 59 | coroutineScope.launch { 60 | offsetX.animateTo( 61 | targetValue = 0f, 62 | animationSpec = tween( 63 | durationMillis = 1000, 64 | delayMillis = 0 65 | ) 66 | ) 67 | } 68 | }, 69 | onDrag = { change, dragAmount -> 70 | change.consume() 71 | coroutineScope.launch { 72 | offsetY.snapTo(offsetY.value + dragAmount.y) 73 | } 74 | coroutineScope.launch { 75 | offsetX.snapTo(offsetX.value + dragAmount.x) 76 | } 77 | } 78 | ) 79 | }, 80 | shape = CircleShape 81 | ) { 82 | Image( 83 | painterResource(R.drawable.logo), 84 | contentDescription = "", 85 | contentScale = ContentScale.Inside 86 | ) 87 | } 88 | } 89 | 90 | @Composable 91 | @ThemePreviews 92 | private fun AnimatedLogoPreview() { 93 | AnimatedLogo() 94 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 11/19/22, 4:16 PM 3 | * Copyright (c) 2022 . All rights reserved. 4 | * Last modified 11/19/22, 3:58 PM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.ui 8 | 9 | import android.os.Build 10 | import android.os.Bundle 11 | import androidx.activity.compose.setContent 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.compose.animation.core.Animatable 14 | import androidx.compose.animation.core.LinearEasing 15 | import androidx.compose.animation.core.RepeatMode 16 | import androidx.compose.animation.core.animateFloat 17 | import androidx.compose.animation.core.infiniteRepeatable 18 | import androidx.compose.animation.core.rememberInfiniteTransition 19 | import androidx.compose.animation.core.tween 20 | import androidx.compose.foundation.Image 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.gestures.detectDragGestures 23 | import androidx.compose.foundation.layout.Arrangement 24 | import androidx.compose.foundation.layout.Column 25 | import androidx.compose.foundation.layout.Row 26 | import androidx.compose.foundation.layout.Spacer 27 | import androidx.compose.foundation.layout.fillMaxHeight 28 | import androidx.compose.foundation.layout.fillMaxWidth 29 | import androidx.compose.foundation.layout.height 30 | import androidx.compose.foundation.layout.offset 31 | import androidx.compose.foundation.layout.padding 32 | import androidx.compose.foundation.layout.size 33 | import androidx.compose.foundation.layout.width 34 | import androidx.compose.foundation.shape.CircleShape 35 | import androidx.compose.material3.Card 36 | import androidx.compose.material3.Icon 37 | import androidx.compose.material3.LinearProgressIndicator 38 | import androidx.compose.material3.MaterialTheme 39 | import androidx.compose.material3.Text 40 | import androidx.compose.runtime.Composable 41 | import androidx.compose.runtime.LaunchedEffect 42 | import androidx.compose.runtime.getValue 43 | import androidx.compose.runtime.mutableIntStateOf 44 | import androidx.compose.runtime.mutableStateOf 45 | import androidx.compose.runtime.remember 46 | import androidx.compose.runtime.rememberCoroutineScope 47 | import androidx.compose.runtime.setValue 48 | import androidx.compose.ui.Alignment 49 | import androidx.compose.ui.Modifier 50 | import androidx.compose.ui.graphics.Color 51 | import androidx.compose.ui.graphics.graphicsLayer 52 | import androidx.compose.ui.input.pointer.pointerInput 53 | import androidx.compose.ui.layout.ContentScale 54 | import androidx.compose.ui.res.painterResource 55 | import androidx.compose.ui.text.style.TextAlign 56 | import androidx.compose.ui.unit.Dp 57 | import androidx.compose.ui.unit.IntOffset 58 | import androidx.compose.ui.unit.dp 59 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 60 | import com.masewsg.app.ui.ComposeApp 61 | import com.masewsg.app.ui.components.ThemePreviews 62 | import com.masewsg.app.ui.components.button.ComposeButton 63 | import com.masewsg.app.ui.components.button.ComposeOutlinedButton 64 | import com.masewsg.app.ui.components.icon.ComposeIcons 65 | import com.masewsg.app.ui.components.theme.ComposeTheme 66 | import com.nphausg.app.embeddedserver.EmbeddedServer 67 | import com.nphausg.app.embeddedserver.R 68 | import kotlinx.coroutines.delay 69 | import kotlinx.coroutines.launch 70 | import kotlin.time.Duration.Companion.seconds 71 | 72 | private val getRunningServerInfo = { ticks: Int -> 73 | "The server is running on: ${Build.MODEL} at ${EmbeddedServer.host} -> (${ticks}s ....)" 74 | } 75 | 76 | class MainActivity : AppCompatActivity() { 77 | 78 | override fun onCreate(savedInstanceState: Bundle?) { 79 | val splashScreen = installSplashScreen() 80 | super.onCreate(savedInstanceState) 81 | // Keep the splash screen on-screen until the UI state is loaded. This condition is 82 | // evaluated each time the app needs to be redrawn so it should be fast to avoid blocking 83 | // the UI. 84 | splashScreen.setKeepOnScreenCondition { 85 | false 86 | } 87 | // Turn off the decor fitting system windows, which allows us to handle insets, 88 | // including IME animations, and go edge-to-edge 89 | // This also sets up the initial system bar style based on the platform theme 90 | // enableEdgeToEdge() 91 | setContent { 92 | ComposeTheme { 93 | ComposeApp { 94 | MainScreen() 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | @Composable 102 | private fun MainScreen(modifier: Modifier = Modifier) { 103 | 104 | var ticks by remember { mutableIntStateOf(0) } 105 | 106 | LaunchedEffect(Unit) { 107 | while (true) { 108 | delay(1.seconds) 109 | ticks++ 110 | } 111 | } 112 | 113 | var hasStarted by remember { mutableStateOf(false) } 114 | 115 | val value by rememberInfiniteTransition(label = "") 116 | .animateFloat( 117 | initialValue = 0.8f, 118 | targetValue = 1f, 119 | animationSpec = infiniteRepeatable( 120 | animation = tween( 121 | durationMillis = 1000, 122 | easing = LinearEasing 123 | ), 124 | repeatMode = RepeatMode.Reverse 125 | ), label = "" 126 | ) 127 | 128 | Column( 129 | modifier = Modifier 130 | .fillMaxWidth() 131 | .fillMaxHeight() 132 | .background(Color.White) 133 | .then(modifier), 134 | horizontalAlignment = Alignment.CenterHorizontally, 135 | verticalArrangement = Arrangement.spacedBy( 136 | space = 20.dp, 137 | alignment = Alignment.CenterVertically 138 | ) 139 | ) { 140 | 141 | val reusedModifier = Modifier.weight(1f) 142 | 143 | Spacer(modifier = reusedModifier) 144 | AnimatedLogo() 145 | Spacer(modifier = Modifier.weight(0.1f)) 146 | Column( 147 | verticalArrangement = Arrangement.spacedBy( 148 | space = 20.dp, 149 | alignment = Alignment.CenterVertically 150 | ) 151 | ) { 152 | Row { 153 | Icon(imageVector = ComposeIcons.PlayArrow, contentDescription = null) 154 | Text( 155 | color = Color.Black, 156 | textAlign = TextAlign.Start, 157 | text = String.format("GET: %s", EmbeddedServer.host), 158 | style = MaterialTheme.typography.titleMedium, 159 | ) 160 | } 161 | Row { 162 | Icon(imageVector = ComposeIcons.PlayArrow, contentDescription = null) 163 | Text( 164 | color = Color.Black, 165 | textAlign = TextAlign.Start, 166 | text = String.format("GET: %s/fruits", EmbeddedServer.host), 167 | style = MaterialTheme.typography.titleMedium, 168 | ) 169 | } 170 | 171 | Row(modifier = Modifier) { 172 | Icon(imageVector = ComposeIcons.PlayArrow, contentDescription = null) 173 | Text( 174 | color = Color.Black, 175 | textAlign = TextAlign.Start, 176 | text = String.format("GET: %s/fruits/{id}", EmbeddedServer.host), 177 | style = MaterialTheme.typography.titleMedium, 178 | ) 179 | } 180 | 181 | Row { 182 | Icon(imageVector = ComposeIcons.PlayArrow, contentDescription = null) 183 | Text( 184 | color = Color.Black, 185 | textAlign = TextAlign.Start, 186 | text = String.format("STATIC: %s/static", EmbeddedServer.host), 187 | style = MaterialTheme.typography.titleMedium, 188 | ) 189 | } 190 | 191 | } 192 | 193 | Row( 194 | modifier = Modifier 195 | .fillMaxWidth() 196 | .padding(Dp(36f)) 197 | ) { 198 | ComposeButton( 199 | enabled = !hasStarted, 200 | modifier = reusedModifier, 201 | onClick = { 202 | hasStarted = true 203 | EmbeddedServer.start() 204 | }, 205 | text = { Text("Start") } 206 | ) 207 | Spacer(modifier = Modifier.weight(0.1f)) 208 | ComposeOutlinedButton( 209 | enabled = hasStarted, 210 | modifier = reusedModifier, 211 | onClick = { 212 | ticks = 0 213 | hasStarted = false 214 | EmbeddedServer.stop() 215 | }, 216 | text = { Text("Stop") } 217 | ) 218 | } 219 | 220 | Column(modifier = Modifier.height(8.dp)) { 221 | if (hasStarted) { 222 | LinearProgressIndicator( 223 | modifier = Modifier.width(64.dp), 224 | color = MaterialTheme.colorScheme.secondary, 225 | trackColor = MaterialTheme.colorScheme.surfaceVariant, 226 | ) 227 | } 228 | } 229 | Text( 230 | modifier = Modifier.graphicsLayer { 231 | if (hasStarted) { 232 | scaleX = value 233 | scaleY = value 234 | } 235 | }, 236 | color = Color.Black, 237 | textAlign = TextAlign.Center, 238 | text = if (hasStarted) { 239 | getRunningServerInfo(ticks) 240 | } else { 241 | "Please click 'Start' to start the embedded server" 242 | }, 243 | style = MaterialTheme.typography.labelMedium, 244 | ) 245 | Spacer(modifier = reusedModifier) 246 | } 247 | } 248 | 249 | @Composable 250 | @ThemePreviews 251 | private fun MainScreenPreview() { 252 | ComposeTheme { 253 | ComposeApp { 254 | MainScreen() 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 4/13/24, 11:46 AM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/13/24, 11:46 AM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.utils 8 | 9 | import kotlinx.serialization.json.Json 10 | 11 | object FileUtils { 12 | fun readText(path: String): String = 13 | this::class.java.classLoader?.getResource(path)?.readText().orEmpty() 14 | 15 | inline fun decode(json: String): T = 16 | Json.decodeFromString(json) 17 | 18 | inline fun readJson(path: String): T = 19 | decode(readText(path)) 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/nphausg/app/embeddedserver/utils/NetworkUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 11/19/22, 4:16 PM 3 | * Copyright (c) 2022 . All rights reserved. 4 | * Last modified 11/19/22, 3:58 PM 5 | */ 6 | 7 | package com.nphausg.app.embeddedserver.utils 8 | 9 | import java.net.InetAddress 10 | import java.net.NetworkInterface 11 | 12 | object NetworkUtils { 13 | 14 | fun getLocalIpAddress(): String? = getInetAddresses() 15 | .filter { it.isLocalAddress() } 16 | .map { it.hostAddress } 17 | .firstOrNull() 18 | 19 | private fun getInetAddresses() = NetworkInterface.getNetworkInterfaces() 20 | .iterator() 21 | .asSequence() 22 | .flatMap { networkInterface -> 23 | networkInterface.inetAddresses 24 | .asSequence() 25 | .filter { !it.isLoopbackAddress } 26 | }.toList() 27 | } 28 | 29 | fun InetAddress.isLocalAddress(): Boolean { 30 | try { 31 | return isSiteLocalAddress 32 | && !hostAddress!!.contains(":") 33 | && hostAddress != "127.0.0.1" 34 | } catch (e: Exception) { 35 | e.printStackTrace() 36 | } 37 | return false 38 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Server 3 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | -------------------------------------------------------------------------------- /app/src/main/resources/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "8fc012d9-ad1d-4519-a339-578cf803d509", 3 | "items": [ 4 | { 5 | "id": "8fc012d9-ad1d-4519-a339-578cf803d509", 6 | "name": "Cucumbers \uD83E\uDD52" 7 | }, 8 | { 9 | "id": "594b8138-ffc5-41cc-b399-a6a699506a73", 10 | "name": "Tomatoes \uD83C\uDF45" 11 | }, 12 | { 13 | "id": "743f4a4d-6a36-4db3-a6d9-5f189943fae5", 14 | "name": "Tomatoes \uD83C\uDF45" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /app/src/main/resources/docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/resources/docs/demo.gif -------------------------------------------------------------------------------- /app/src/main/resources/docs/detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/resources/docs/detail.jpg -------------------------------------------------------------------------------- /app/src/main/resources/docs/edge_get.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/resources/docs/edge_get.gif -------------------------------------------------------------------------------- /app/src/main/resources/docs/fruits.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/resources/docs/fruits.jpg -------------------------------------------------------------------------------- /app/src/main/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/resources/favicon.ico -------------------------------------------------------------------------------- /app/src/main/resources/files/file.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/app/src/main/resources/files/file.jpg -------------------------------------------------------------------------------- /app/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | Android Embedded Server 14 | 32 | 33 | 34 | 35 | 36 |

Android Embedded Server (@nphausg)

37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | Build Status 47 |
48 | 49 |

50 | ----------------------------------------------------- 52 |

53 | 54 |
55 | 56 |

👉 Overview

57 |

A minimal way to create HTTP server in android with Kotlin. Create asynchronous client and server 58 | applications. 59 | Anything from microservices to multiplatform HTTP client apps in a simple way. Open Source, free, and fun! 60 |

61 |
embeddedServer(Netty, PORT, watchPaths = emptyList()) {
 62 |             install(WebSockets)
 63 |             install(CallLogging)
 64 |             routing {
 65 |                 get("/") {
 66 |                     call.respondText(
 67 |                         text = "Hello!! You are here in ${Build.MODEL}",
 68 |                         contentType = ContentType.Text.Plain
 69 |                     )
 70 |                 }
 71 |             }
 72 |         }
 73 | 
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
Image 1Image 2Image 3Image 4
82 |

🚀 How to use

83 |

Cloning the repository into a local directory and checkout the desired branch:

84 |
git clone git@github.com:nphausg/android.embeddedserver.git
 85 | cd android.embeddedserver
 86 | git checkout master
 87 | 
88 |

✨ Contributing

89 |

Please feel free to contact me or make a pull request.

90 |

👀 Author

91 |

92 | 93 | 95 | 96 | 97 | 98 | 99 |

100 |
101 | 104 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath libs.tools.gradle 9 | classpath libs.tools.kotlin 10 | } 11 | 12 | } 13 | 14 | plugins { 15 | id 'org.jetbrains.kotlin.android' version '1.9.23' apply false 16 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23' apply false 17 | } 18 | 19 | tasks.register('clean', Delete) { 20 | delete rootProject.buildDir 21 | } -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/docs/demo.gif -------------------------------------------------------------------------------- /docs/detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/docs/detail.jpg -------------------------------------------------------------------------------- /docs/edge_get.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/docs/edge_get.gif -------------------------------------------------------------------------------- /docs/fruits.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/docs/fruits.jpg -------------------------------------------------------------------------------- /docs/static_config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/docs/static_config.jpg -------------------------------------------------------------------------------- /docs/static_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/docs/static_demo.jpg -------------------------------------------------------------------------------- /foundation/ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /foundation/ui/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.masewsg.app.ui' 8 | compileSdk 34 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 34 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_17 25 | targetCompatibility JavaVersion.VERSION_17 26 | } 27 | kotlinOptions { 28 | jvmTarget = JavaVersion.VERSION_17 29 | } 30 | buildFeatures { 31 | compose true 32 | } 33 | composeOptions { 34 | kotlinCompilerExtensionVersion = "1.5.11" 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation libs.androidx.core.ktx 40 | implementation libs.androidx.appcompat 41 | 42 | // Compose 43 | implementation platform(libs.androidx.compose.bom) 44 | api libs.androidx.material3 45 | // Optional - Integration with activities 46 | api libs.androidx.activity.compose 47 | // or only import the main APIs for the underlying toolkit systems, 48 | // such as input and measurement/layout 49 | api libs.androidx.ui 50 | debugApi libs.androidx.ui.tooling 51 | // Android Studio Preview support 52 | api libs.androidx.ui.tooling.preview 53 | api libs.androidx.ui.graphics 54 | api libs.material3 55 | api libs.androidx.foundation 56 | api libs.navigation.compose 57 | } -------------------------------------------------------------------------------- /foundation/ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphausg/android.embeddedserver/41ba38d1ea2e9fb2401b2fdb4fd57b7343bb738f/foundation/ui/consumer-rules.pro -------------------------------------------------------------------------------- /foundation/ui/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /foundation/ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/ComposeApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:36 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 5/4/24, 10:35 PM 5 | */ 6 | 7 | package com.masewsg.app.ui 8 | 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.WindowInsets 11 | import androidx.compose.foundation.layout.WindowInsetsSides 12 | import androidx.compose.foundation.layout.consumeWindowInsets 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.only 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.safeDrawing 17 | import androidx.compose.foundation.layout.windowInsetsPadding 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.Scaffold 20 | import androidx.compose.material3.SnackbarHost 21 | import androidx.compose.material3.SnackbarHostState 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.getValue 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.runtime.saveable.rememberSaveable 27 | import androidx.compose.ui.ExperimentalComposeUiApi 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.semantics.semantics 31 | import androidx.compose.ui.semantics.testTagsAsResourceId 32 | import com.masewsg.app.ui.components.ComposeBackground 33 | import com.masewsg.app.ui.components.ComposeGradientBackground 34 | import com.masewsg.app.ui.components.color.GradientColors 35 | import com.masewsg.app.ui.components.color.LocalGradientColors 36 | 37 | @OptIn(ExperimentalComposeUiApi::class) 38 | @Composable 39 | fun ComposeApp(content: @Composable () -> Unit) { 40 | 41 | val shouldShowGradientBackground by rememberSaveable { mutableStateOf(false) } 42 | 43 | ComposeBackground { 44 | ComposeGradientBackground( 45 | gradientColors = if (shouldShowGradientBackground) { 46 | LocalGradientColors.current 47 | } else { 48 | GradientColors() 49 | }, 50 | ) { 51 | val snackbarHostState = remember { SnackbarHostState() } 52 | Scaffold( 53 | modifier = Modifier.semantics { 54 | testTagsAsResourceId = true 55 | }, 56 | containerColor = Color.Transparent, 57 | contentColor = Color.Green, 58 | contentWindowInsets = WindowInsets(0, 0, 0, 0), 59 | snackbarHost = { SnackbarHost(snackbarHostState) }, 60 | bottomBar = { 61 | 62 | }, 63 | ) { padding -> 64 | Row( 65 | Modifier 66 | .fillMaxSize() 67 | .padding(padding) 68 | .consumeWindowInsets(padding) 69 | .windowInsetsPadding( 70 | WindowInsets.safeDrawing.only( 71 | WindowInsetsSides.Horizontal, 72 | ), 73 | ), 74 | ) { 75 | content() 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/Background.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:36 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 7:59 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components 8 | 9 | import android.content.res.Configuration 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material3.LocalAbsoluteTonalElevation 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.CompositionLocalProvider 17 | import androidx.compose.runtime.Immutable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.rememberUpdatedState 20 | import androidx.compose.runtime.staticCompositionLocalOf 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.drawWithCache 23 | import androidx.compose.ui.geometry.Offset 24 | import androidx.compose.ui.graphics.Brush 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.Dp 28 | import androidx.compose.ui.unit.dp 29 | import com.masewsg.app.ui.components.color.GradientColors 30 | import com.masewsg.app.ui.components.color.LocalGradientColors 31 | import com.masewsg.app.ui.components.theme.ComposeTheme 32 | import kotlin.math.tan 33 | 34 | /** 35 | * A class to model background color and tonal elevation values for Now in Android. 36 | */ 37 | @Immutable 38 | data class BackgroundTheme( 39 | val color: Color = Color.Unspecified, 40 | val tonalElevation: Dp = Dp.Unspecified, 41 | ) 42 | 43 | /** 44 | * A composition local for [BackgroundTheme]. 45 | */ 46 | val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() } 47 | 48 | 49 | /** 50 | * The main background for the app. 51 | * Uses [LocalBackgroundTheme] to set the color and tonal elevation of a [Surface]. 52 | * 53 | * @param modifier Modifier to be applied to the background. 54 | * @param content The background content. 55 | */ 56 | @Composable 57 | fun ComposeBackground( 58 | modifier: Modifier = Modifier, 59 | content: @Composable () -> Unit, 60 | ) { 61 | val color = LocalBackgroundTheme.current.color 62 | val tonalElevation = LocalBackgroundTheme.current.tonalElevation 63 | Surface( 64 | color = if (color == Color.Unspecified) Color.Transparent else color, 65 | tonalElevation = if (tonalElevation == Dp.Unspecified) 0.dp else tonalElevation, 66 | modifier = modifier.fillMaxSize(), 67 | ) { 68 | CompositionLocalProvider(LocalAbsoluteTonalElevation provides 0.dp) { 69 | content() 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * A gradient background for select screens. Uses [LocalBackgroundTheme] to set the gradient colors 76 | * of a [Box] within a [Surface]. 77 | * 78 | * @param modifier Modifier to be applied to the background. 79 | * @param gradientColors The gradient colors to be rendered. 80 | * @param content The background content. 81 | */ 82 | @Composable 83 | fun ComposeGradientBackground( 84 | modifier: Modifier = Modifier, 85 | gradientColors: GradientColors = LocalGradientColors.current, 86 | content: @Composable () -> Unit, 87 | ) { 88 | val currentTopColor by rememberUpdatedState(gradientColors.top) 89 | val currentBottomColor by rememberUpdatedState(gradientColors.bottom) 90 | Surface( 91 | color = if (gradientColors.container == Color.Unspecified) { 92 | Color.Transparent 93 | } else { 94 | gradientColors.container 95 | }, 96 | modifier = modifier.fillMaxSize(), 97 | ) { 98 | Box( 99 | Modifier 100 | .fillMaxSize() 101 | .drawWithCache { 102 | // Compute the start and end coordinates such that the gradients are angled 11.06 103 | // degrees off the vertical axis 104 | val offset = size.height * tan( 105 | Math 106 | .toRadians(11.06) 107 | .toFloat(), 108 | ) 109 | 110 | val start = Offset(size.width / 2 + offset / 2, 0f) 111 | val end = Offset(size.width / 2 - offset / 2, size.height) 112 | 113 | // Create the top gradient that fades out after the halfway point vertically 114 | val topGradient = Brush.linearGradient( 115 | 0f to if (currentTopColor == Color.Unspecified) { 116 | Color.Transparent 117 | } else { 118 | currentTopColor 119 | }, 120 | 0.724f to Color.Transparent, 121 | start = start, 122 | end = end, 123 | ) 124 | // Create the bottom gradient that fades in before the halfway point vertically 125 | val bottomGradient = Brush.linearGradient( 126 | 0.2552f to Color.Transparent, 127 | 1f to if (currentBottomColor == Color.Unspecified) { 128 | Color.Transparent 129 | } else { 130 | currentBottomColor 131 | }, 132 | start = start, 133 | end = end, 134 | ) 135 | 136 | onDrawBehind { 137 | // There is overlap here, so order is important 138 | drawRect(topGradient) 139 | drawRect(bottomGradient) 140 | } 141 | }, 142 | ) { 143 | content() 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Multi Preview annotation that represents light and dark themes. Add this annotation to a 150 | * composable to render the both themes. 151 | */ 152 | @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") 153 | @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") 154 | annotation class ThemePreviews 155 | 156 | @ThemePreviews 157 | @Composable 158 | fun BackgroundDefault() { 159 | ComposeTheme(disableDynamicTheming = true) { 160 | ComposeBackground(Modifier.size(100.dp), content = {}) 161 | } 162 | } 163 | 164 | @ThemePreviews 165 | @Composable 166 | fun BackgroundDynamic() { 167 | ComposeTheme(disableDynamicTheming = false) { 168 | ComposeBackground(Modifier.size(100.dp), content = {}) 169 | } 170 | } 171 | 172 | @ThemePreviews 173 | @Composable 174 | fun BackgroundAndroid() { 175 | ComposeTheme(androidTheme = true) { 176 | ComposeBackground(Modifier.size(100.dp), content = {}) 177 | } 178 | } 179 | 180 | @ThemePreviews 181 | @Composable 182 | fun GradientBackgroundDefault() { 183 | ComposeTheme(disableDynamicTheming = true) { 184 | ComposeGradientBackground(Modifier.size(100.dp), content = {}) 185 | } 186 | } 187 | 188 | @ThemePreviews 189 | @Composable 190 | fun GradientBackgroundDynamic() { 191 | ComposeTheme(disableDynamicTheming = false) { 192 | ComposeGradientBackground(Modifier.size(100.dp), content = {}) 193 | } 194 | } 195 | 196 | @ThemePreviews 197 | @Composable 198 | fun GradientBackgroundAndroid() { 199 | ComposeTheme(androidTheme = true) { 200 | ComposeGradientBackground(Modifier.size(100.dp), content = {}) 201 | } 202 | } 203 | 204 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/button/Button.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:36 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 8:38 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.button 8 | 9 | import androidx.compose.foundation.BorderStroke 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.PaddingValues 12 | import androidx.compose.foundation.layout.RowScope 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.sizeIn 16 | import androidx.compose.material3.Button 17 | import androidx.compose.material3.ButtonDefaults 18 | import androidx.compose.material3.FilledIconToggleButton 19 | import androidx.compose.material3.Icon 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.OutlinedButton 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TextButton 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.unit.dp 31 | import com.masewsg.app.ui.components.ComposeBackground 32 | import com.masewsg.app.ui.components.ThemePreviews 33 | import com.masewsg.app.ui.components.icon.ComposeIcons 34 | import com.masewsg.app.ui.components.theme.ComposeTheme 35 | 36 | /** 37 | * Now in Android filled button with generic content slot. Wraps Material 3 [Button]. 38 | * 39 | * @param onClick Will be called when the user clicks the button. 40 | * @param modifier Modifier to be applied to the button. 41 | * @param enabled Controls the enabled state of the button. When `false`, this button will not be 42 | * clickable and will appear disabled to accessibility services. 43 | * @param contentPadding The spacing values to apply internally between the container and the 44 | * content. 45 | * @param content The button content. 46 | */ 47 | @Composable 48 | fun ComposeButton( 49 | onClick: () -> Unit, 50 | modifier: Modifier = Modifier, 51 | enabled: Boolean = true, 52 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 53 | content: @Composable RowScope.() -> Unit, 54 | ) { 55 | Button( 56 | onClick = onClick, 57 | modifier = modifier, 58 | enabled = enabled, 59 | colors = ButtonDefaults.buttonColors( 60 | containerColor = MaterialTheme.colorScheme.onBackground, 61 | ), 62 | contentPadding = contentPadding, 63 | content = content, 64 | ) 65 | } 66 | 67 | /** 68 | * Now in Android filled button with text and icon content slots. 69 | * 70 | * @param onClick Will be called when the user clicks the button. 71 | * @param modifier Modifier to be applied to the button. 72 | * @param enabled Controls the enabled state of the button. When `false`, this button will not be 73 | * clickable and will appear disabled to accessibility services. 74 | * @param text The button text label content. 75 | * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. 76 | */ 77 | @Composable 78 | fun ComposeButton( 79 | onClick: () -> Unit, 80 | modifier: Modifier = Modifier, 81 | enabled: Boolean = true, 82 | text: @Composable () -> Unit, 83 | leadingIcon: @Composable (() -> Unit)? = null, 84 | ) { 85 | ComposeButton( 86 | onClick = onClick, 87 | modifier = modifier, 88 | enabled = enabled, 89 | contentPadding = if (leadingIcon != null) { 90 | ButtonDefaults.ButtonWithIconContentPadding 91 | } else { 92 | ButtonDefaults.ContentPadding 93 | }, 94 | ) { 95 | ComposeButtonContent( 96 | text = text, 97 | leadingIcon = leadingIcon, 98 | ) 99 | } 100 | } 101 | 102 | /** 103 | * Now in Android outlined button with generic content slot. Wraps Material 3 [OutlinedButton]. 104 | * 105 | * @param onClick Will be called when the user clicks the button. 106 | * @param modifier Modifier to be applied to the button. 107 | * @param enabled Controls the enabled state of the button. When `false`, this button will not be 108 | * clickable and will appear disabled to accessibility services. 109 | * @param contentPadding The spacing values to apply internally between the container and the 110 | * content. 111 | * @param content The button content. 112 | */ 113 | @Composable 114 | fun ComposeOutlinedButton( 115 | onClick: () -> Unit, 116 | modifier: Modifier = Modifier, 117 | enabled: Boolean = true, 118 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 119 | content: @Composable RowScope.() -> Unit, 120 | ) { 121 | OutlinedButton( 122 | onClick = onClick, 123 | modifier = modifier, 124 | enabled = enabled, 125 | colors = ButtonDefaults.outlinedButtonColors( 126 | contentColor = MaterialTheme.colorScheme.onBackground, 127 | ), 128 | border = BorderStroke( 129 | width = ComposeButtonDefaults.OutlinedButtonBorderWidth, 130 | color = if (enabled) { 131 | MaterialTheme.colorScheme.outline 132 | } else { 133 | MaterialTheme.colorScheme.onSurface.copy( 134 | alpha = ComposeButtonDefaults.DISABLED_OUTLINED_BUTTON_BORDER_ALPHA, 135 | ) 136 | }, 137 | ), 138 | contentPadding = contentPadding, 139 | content = content, 140 | ) 141 | } 142 | 143 | /** 144 | * Now in Android outlined button with text and icon content slots. 145 | * 146 | * @param onClick Will be called when the user clicks the button. 147 | * @param modifier Modifier to be applied to the button. 148 | * @param enabled Controls the enabled state of the button. When `false`, this button will not be 149 | * clickable and will appear disabled to accessibility services. 150 | * @param text The button text label content. 151 | * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. 152 | */ 153 | @Composable 154 | fun ComposeOutlinedButton( 155 | onClick: () -> Unit, 156 | modifier: Modifier = Modifier, 157 | enabled: Boolean = true, 158 | text: @Composable () -> Unit, 159 | leadingIcon: @Composable (() -> Unit)? = null, 160 | ) { 161 | ComposeOutlinedButton( 162 | onClick = onClick, 163 | modifier = modifier, 164 | enabled = enabled, 165 | contentPadding = if (leadingIcon != null) { 166 | ButtonDefaults.ButtonWithIconContentPadding 167 | } else { 168 | ButtonDefaults.ContentPadding 169 | }, 170 | ) { 171 | ComposeButtonContent( 172 | text = text, 173 | leadingIcon = leadingIcon, 174 | ) 175 | } 176 | } 177 | 178 | /** 179 | * Now in Android text button with generic content slot. Wraps Material 3 [TextButton]. 180 | * 181 | * @param onClick Will be called when the user clicks the button. 182 | * @param modifier Modifier to be applied to the button. 183 | * @param enabled Controls the enabled state of the button. When `false`, this button will not be 184 | * clickable and will appear disabled to accessibility services. 185 | * @param content The button content. 186 | */ 187 | @Composable 188 | fun ComposeTextButton( 189 | onClick: () -> Unit, 190 | modifier: Modifier = Modifier, 191 | enabled: Boolean = true, 192 | content: @Composable RowScope.() -> Unit, 193 | ) { 194 | TextButton( 195 | onClick = onClick, 196 | modifier = modifier, 197 | enabled = enabled, 198 | colors = ButtonDefaults.textButtonColors( 199 | contentColor = MaterialTheme.colorScheme.onBackground, 200 | ), 201 | content = content, 202 | ) 203 | } 204 | 205 | /** 206 | * Now in Android text button with text and icon content slots. 207 | * 208 | * @param onClick Will be called when the user clicks the button. 209 | * @param modifier Modifier to be applied to the button. 210 | * @param enabled Controls the enabled state of the button. When `false`, this button will not be 211 | * clickable and will appear disabled to accessibility services. 212 | * @param text The button text label content. 213 | * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. 214 | */ 215 | @Composable 216 | fun ComposeTextButton( 217 | onClick: () -> Unit, 218 | modifier: Modifier = Modifier, 219 | enabled: Boolean = true, 220 | text: @Composable () -> Unit, 221 | leadingIcon: @Composable (() -> Unit)? = null, 222 | ) { 223 | ComposeTextButton( 224 | onClick = onClick, 225 | modifier = modifier, 226 | enabled = enabled, 227 | ) { 228 | ComposeButtonContent( 229 | text = text, 230 | leadingIcon = leadingIcon, 231 | ) 232 | } 233 | } 234 | 235 | /** 236 | * Internal Now in Android button content layout for arranging the text label and leading icon. 237 | * 238 | * @param text The button text label content. 239 | * @param leadingIcon The button leading icon content. Default is `null` for no leading icon.Ï 240 | */ 241 | @Composable 242 | private fun ComposeButtonContent( 243 | text: @Composable () -> Unit, 244 | leadingIcon: @Composable (() -> Unit)? = null, 245 | ) { 246 | if (leadingIcon != null) { 247 | Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) { 248 | leadingIcon() 249 | } 250 | } 251 | Box( 252 | Modifier 253 | .padding( 254 | start = if (leadingIcon != null) { 255 | ButtonDefaults.IconSpacing 256 | } else { 257 | 0.dp 258 | }, 259 | ), 260 | ) { 261 | text() 262 | } 263 | } 264 | 265 | @Composable 266 | fun ComposeToggleButton(checked: @Composable () -> Unit, unchecked: @Composable () -> Unit) { 267 | var hasChecked by remember { mutableStateOf(false) } 268 | FilledIconToggleButton(checked = hasChecked, onCheckedChange = { hasChecked = it }) { 269 | if (hasChecked) { 270 | checked() 271 | } else { 272 | unchecked() 273 | } 274 | } 275 | } 276 | 277 | @ThemePreviews 278 | @Composable 279 | fun ComposeButtonPreview() { 280 | ComposeTheme { 281 | ComposeBackground(modifier = Modifier.size(150.dp, 50.dp)) { 282 | ComposeButton(onClick = {}, text = { Text("Test button") }) 283 | } 284 | } 285 | } 286 | 287 | @ThemePreviews 288 | @Composable 289 | fun ComposeOutlinedButtonPreview() { 290 | ComposeTheme { 291 | ComposeBackground(modifier = Modifier.size(150.dp, 50.dp)) { 292 | ComposeOutlinedButton(onClick = {}, text = { Text("Test button") }) 293 | } 294 | } 295 | } 296 | 297 | @ThemePreviews 298 | @Composable 299 | fun ComposeButtonLeadingIconPreview() { 300 | ComposeTheme { 301 | ComposeBackground(modifier = Modifier.size(150.dp, 50.dp)) { 302 | ComposeButton( 303 | onClick = {}, 304 | text = { Text("Test button") }, 305 | leadingIcon = { Icon(imageVector = ComposeIcons.Info, contentDescription = null) }, 306 | ) 307 | } 308 | } 309 | } 310 | 311 | /** 312 | * Now in Android button default values. 313 | */ 314 | object ComposeButtonDefaults { 315 | // OutlinedButton border color doesn't respect disabled state by default 316 | const val DISABLED_OUTLINED_BUTTON_BORDER_ALPHA = 0.12f 317 | 318 | // OutlinedButton default border width isn't exposed via ButtonDefaults 319 | val OutlinedButtonBorderWidth = 1.dp 320 | } 321 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/color/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 4/10/24, 8:29 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 8:29 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.color 8 | 9 | import androidx.compose.ui.graphics.Color 10 | 11 | internal val Blue10 = Color(0xFF001F28) 12 | internal val Blue20 = Color(0xFF003544) 13 | internal val Blue30 = Color(0xFF004D61) 14 | internal val Blue40 = Color(0xFF006780) 15 | internal val Blue80 = Color(0xFF5DD5FC) 16 | internal val Blue90 = Color(0xFFB8EAFF) 17 | internal val DarkGreen10 = Color(0xFF0D1F12) 18 | internal val DarkGreen20 = Color(0xFF223526) 19 | internal val DarkGreen30 = Color(0xFF394B3C) 20 | internal val DarkGreen40 = Color(0xFF4F6352) 21 | internal val DarkGreen80 = Color(0xFFB7CCB8) 22 | internal val DarkGreen90 = Color(0xFFD3E8D3) 23 | internal val DarkGreenGray10 = Color(0xFF1A1C1A) 24 | internal val DarkGreenGray20 = Color(0xFF2F312E) 25 | internal val DarkGreenGray90 = Color(0xFFE2E3DE) 26 | internal val DarkGreenGray95 = Color(0xFFF0F1EC) 27 | internal val DarkGreenGray99 = Color(0xFFFBFDF7) 28 | internal val DarkPurpleGray10 = Color(0xFF201A1B) 29 | internal val DarkPurpleGray20 = Color(0xFF362F30) 30 | internal val DarkPurpleGray90 = Color(0xFFECDFE0) 31 | internal val DarkPurpleGray95 = Color(0xFFFAEEEF) 32 | internal val DarkPurpleGray99 = Color(0xFFFCFCFC) 33 | internal val Green10 = Color(0xFF00210B) 34 | internal val Green20 = Color(0xFF003919) 35 | internal val Green30 = Color(0xFF005227) 36 | internal val Green40 = Color(0xFF006D36) 37 | internal val Green80 = Color(0xFF0EE37C) 38 | internal val Green90 = Color(0xFF5AFF9D) 39 | internal val GreenGray30 = Color(0xFF414941) 40 | internal val GreenGray50 = Color(0xFF727971) 41 | internal val GreenGray60 = Color(0xFF8B938A) 42 | internal val GreenGray80 = Color(0xFFC1C9BF) 43 | internal val GreenGray90 = Color(0xFFDDE5DB) 44 | internal val Orange10 = Color(0xFF380D00) 45 | internal val Orange20 = Color(0xFF5B1A00) 46 | internal val Orange30 = Color(0xFF812800) 47 | internal val Orange40 = Color(0xFFA23F16) 48 | internal val Orange80 = Color(0xFFFFB59B) 49 | internal val Orange90 = Color(0xFFFFDBCF) 50 | internal val Purple10 = Color(0xFF36003C) 51 | internal val Purple20 = Color(0xFF560A5D) 52 | internal val Purple30 = Color(0xFF702776) 53 | internal val Purple40 = Color(0xFF8B418F) 54 | internal val Purple80 = Color(0xFFFFA9FE) 55 | internal val Purple90 = Color(0xFFFFD6FA) 56 | internal val PurpleGray30 = Color(0xFF4D444C) 57 | internal val PurpleGray50 = Color(0xFF7F747C) 58 | internal val PurpleGray60 = Color(0xFF998D96) 59 | internal val PurpleGray80 = Color(0xFFD0C3CC) 60 | internal val PurpleGray90 = Color(0xFFEDDEE8) 61 | internal val Red10 = Color(0xFF410002) 62 | internal val Red20 = Color(0xFF690005) 63 | internal val Red30 = Color(0xFF93000A) 64 | internal val Red40 = Color(0xFFBA1A1A) 65 | internal val Red80 = Color(0xFFFFB4AB) 66 | internal val Red90 = Color(0xFFFFDAD6) 67 | internal val Teal10 = Color(0xFF001F26) 68 | internal val Teal20 = Color(0xFF02363F) 69 | internal val Teal30 = Color(0xFF214D56) 70 | internal val Teal40 = Color(0xFF3A656F) 71 | internal val Teal80 = Color(0xFFA2CED9) 72 | internal val Teal90 = Color(0xFFBEEAF6) 73 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/color/Gradient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:36 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 7:58 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.color 8 | 9 | import androidx.compose.runtime.Immutable 10 | import androidx.compose.runtime.staticCompositionLocalOf 11 | import androidx.compose.ui.graphics.Color 12 | 13 | /** 14 | * A class to model gradient color values for Now in Android. 15 | * 16 | * @param top The top gradient color to be rendered. 17 | * @param bottom The bottom gradient color to be rendered. 18 | * @param container The container gradient color over which the gradient will be rendered. 19 | */ 20 | @Immutable 21 | data class GradientColors( 22 | val top: Color = Color.Unspecified, 23 | val bottom: Color = Color.Unspecified, 24 | val container: Color = Color.Unspecified, 25 | ) 26 | 27 | /** 28 | * A composition local for [GradientColors]. 29 | */ 30 | val LocalGradientColors = staticCompositionLocalOf { GradientColors() } 31 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/color/Tint.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:36 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 7:59 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.color 8 | 9 | import androidx.compose.runtime.Immutable 10 | import androidx.compose.runtime.staticCompositionLocalOf 11 | import androidx.compose.ui.graphics.Color 12 | 13 | /** 14 | * A class to model background color and tonal elevation values for Now in Android. 15 | */ 16 | @Immutable 17 | data class TintTheme( 18 | val iconTint: Color = Color.Unspecified, 19 | ) 20 | 21 | /** 22 | * A composition local for [TintTheme]. 23 | */ 24 | val LocalTintTheme = staticCompositionLocalOf { TintTheme() } 25 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/icon/Icons.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:40 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 5/4/24, 10:36 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.icon 8 | 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.Info 11 | import androidx.compose.material.icons.outlined.PlayArrow 12 | import androidx.compose.ui.graphics.vector.ImageVector 13 | 14 | /** 15 | * Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. 16 | */ 17 | object ComposeIcons { 18 | val Info = Icons.Outlined.Info 19 | val PlayArrow = Icons.Outlined.PlayArrow 20 | } 21 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 5/4/24, 10:36 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 8:11 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.theme 8 | 9 | import android.os.Build 10 | import androidx.annotation.ChecksSdkIntAtLeast 11 | import androidx.annotation.VisibleForTesting 12 | import androidx.compose.foundation.isSystemInDarkTheme 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.darkColorScheme 15 | import androidx.compose.material3.dynamicDarkColorScheme 16 | import androidx.compose.material3.dynamicLightColorScheme 17 | import androidx.compose.material3.lightColorScheme 18 | import androidx.compose.material3.surfaceColorAtElevation 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.CompositionLocalProvider 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.unit.dp 24 | import com.masewsg.app.ui.components.BackgroundTheme 25 | import com.masewsg.app.ui.components.LocalBackgroundTheme 26 | import com.masewsg.app.ui.components.color.Blue10 27 | import com.masewsg.app.ui.components.color.Blue20 28 | import com.masewsg.app.ui.components.color.Blue30 29 | import com.masewsg.app.ui.components.color.Blue40 30 | import com.masewsg.app.ui.components.color.Blue80 31 | import com.masewsg.app.ui.components.color.Blue90 32 | import com.masewsg.app.ui.components.color.DarkGreen10 33 | import com.masewsg.app.ui.components.color.DarkGreen20 34 | import com.masewsg.app.ui.components.color.DarkGreen30 35 | import com.masewsg.app.ui.components.color.DarkGreen40 36 | import com.masewsg.app.ui.components.color.DarkGreen80 37 | import com.masewsg.app.ui.components.color.DarkGreen90 38 | import com.masewsg.app.ui.components.color.DarkGreenGray10 39 | import com.masewsg.app.ui.components.color.DarkGreenGray20 40 | import com.masewsg.app.ui.components.color.DarkGreenGray90 41 | import com.masewsg.app.ui.components.color.DarkGreenGray95 42 | import com.masewsg.app.ui.components.color.DarkGreenGray99 43 | import com.masewsg.app.ui.components.color.DarkPurpleGray10 44 | import com.masewsg.app.ui.components.color.DarkPurpleGray20 45 | import com.masewsg.app.ui.components.color.DarkPurpleGray90 46 | import com.masewsg.app.ui.components.color.DarkPurpleGray95 47 | import com.masewsg.app.ui.components.color.DarkPurpleGray99 48 | import com.masewsg.app.ui.components.color.GradientColors 49 | import com.masewsg.app.ui.components.color.Green10 50 | import com.masewsg.app.ui.components.color.Green20 51 | import com.masewsg.app.ui.components.color.Green30 52 | import com.masewsg.app.ui.components.color.Green40 53 | import com.masewsg.app.ui.components.color.Green80 54 | import com.masewsg.app.ui.components.color.Green90 55 | import com.masewsg.app.ui.components.color.GreenGray30 56 | import com.masewsg.app.ui.components.color.GreenGray50 57 | import com.masewsg.app.ui.components.color.GreenGray60 58 | import com.masewsg.app.ui.components.color.GreenGray80 59 | import com.masewsg.app.ui.components.color.GreenGray90 60 | import com.masewsg.app.ui.components.color.LocalGradientColors 61 | import com.masewsg.app.ui.components.color.LocalTintTheme 62 | import com.masewsg.app.ui.components.color.Orange10 63 | import com.masewsg.app.ui.components.color.Orange20 64 | import com.masewsg.app.ui.components.color.Orange30 65 | import com.masewsg.app.ui.components.color.Orange40 66 | import com.masewsg.app.ui.components.color.Orange80 67 | import com.masewsg.app.ui.components.color.Orange90 68 | import com.masewsg.app.ui.components.color.Purple10 69 | import com.masewsg.app.ui.components.color.Purple20 70 | import com.masewsg.app.ui.components.color.Purple30 71 | import com.masewsg.app.ui.components.color.Purple40 72 | import com.masewsg.app.ui.components.color.Purple80 73 | import com.masewsg.app.ui.components.color.Purple90 74 | import com.masewsg.app.ui.components.color.PurpleGray30 75 | import com.masewsg.app.ui.components.color.PurpleGray50 76 | import com.masewsg.app.ui.components.color.PurpleGray60 77 | import com.masewsg.app.ui.components.color.PurpleGray80 78 | import com.masewsg.app.ui.components.color.PurpleGray90 79 | import com.masewsg.app.ui.components.color.Red10 80 | import com.masewsg.app.ui.components.color.Red20 81 | import com.masewsg.app.ui.components.color.Red30 82 | import com.masewsg.app.ui.components.color.Red40 83 | import com.masewsg.app.ui.components.color.Red80 84 | import com.masewsg.app.ui.components.color.Red90 85 | import com.masewsg.app.ui.components.color.Teal10 86 | import com.masewsg.app.ui.components.color.Teal20 87 | import com.masewsg.app.ui.components.color.Teal30 88 | import com.masewsg.app.ui.components.color.Teal40 89 | import com.masewsg.app.ui.components.color.Teal80 90 | import com.masewsg.app.ui.components.color.Teal90 91 | import com.masewsg.app.ui.components.color.TintTheme 92 | import com.masewsg.app.ui.components.typography.Typography 93 | 94 | /** 95 | * Light default theme color scheme 96 | */ 97 | @VisibleForTesting 98 | val LightDefaultColorScheme = lightColorScheme( 99 | primary = Purple40, 100 | onPrimary = Color.White, 101 | primaryContainer = Purple90, 102 | onPrimaryContainer = Purple10, 103 | secondary = Orange40, 104 | onSecondary = Color.White, 105 | secondaryContainer = Orange90, 106 | onSecondaryContainer = Orange10, 107 | tertiary = Blue40, 108 | onTertiary = Color.White, 109 | tertiaryContainer = Blue90, 110 | onTertiaryContainer = Blue10, 111 | error = Red40, 112 | onError = Color.White, 113 | errorContainer = Red90, 114 | onErrorContainer = Red10, 115 | background = DarkPurpleGray99, 116 | onBackground = DarkPurpleGray10, 117 | surface = DarkPurpleGray99, 118 | onSurface = DarkPurpleGray10, 119 | surfaceVariant = PurpleGray90, 120 | onSurfaceVariant = PurpleGray30, 121 | inverseSurface = DarkPurpleGray20, 122 | inverseOnSurface = DarkPurpleGray95, 123 | outline = PurpleGray50, 124 | ) 125 | 126 | /** 127 | * Dark default theme color scheme 128 | */ 129 | @VisibleForTesting 130 | val DarkDefaultColorScheme = darkColorScheme( 131 | primary = Purple80, 132 | onPrimary = Purple20, 133 | primaryContainer = Purple30, 134 | onPrimaryContainer = Purple90, 135 | secondary = Orange80, 136 | onSecondary = Orange20, 137 | secondaryContainer = Orange30, 138 | onSecondaryContainer = Orange90, 139 | tertiary = Blue80, 140 | onTertiary = Blue20, 141 | tertiaryContainer = Blue30, 142 | onTertiaryContainer = Blue90, 143 | error = Red80, 144 | onError = Red20, 145 | errorContainer = Red30, 146 | onErrorContainer = Red90, 147 | background = DarkPurpleGray10, 148 | onBackground = DarkPurpleGray90, 149 | surface = DarkPurpleGray10, 150 | onSurface = DarkPurpleGray90, 151 | surfaceVariant = PurpleGray30, 152 | onSurfaceVariant = PurpleGray80, 153 | inverseSurface = DarkPurpleGray90, 154 | inverseOnSurface = DarkPurpleGray10, 155 | outline = PurpleGray60, 156 | ) 157 | 158 | /** 159 | * Light Android theme color scheme 160 | */ 161 | @VisibleForTesting 162 | val LightAndroidColorScheme = lightColorScheme( 163 | primary = Green40, 164 | onPrimary = Color.White, 165 | primaryContainer = Green90, 166 | onPrimaryContainer = Green10, 167 | secondary = DarkGreen40, 168 | onSecondary = Color.White, 169 | secondaryContainer = DarkGreen90, 170 | onSecondaryContainer = DarkGreen10, 171 | tertiary = Teal40, 172 | onTertiary = Color.White, 173 | tertiaryContainer = Teal90, 174 | onTertiaryContainer = Teal10, 175 | error = Red40, 176 | onError = Color.White, 177 | errorContainer = Red90, 178 | onErrorContainer = Red10, 179 | background = DarkGreenGray99, 180 | onBackground = DarkGreenGray10, 181 | surface = DarkGreenGray99, 182 | onSurface = DarkGreenGray10, 183 | surfaceVariant = GreenGray90, 184 | onSurfaceVariant = GreenGray30, 185 | inverseSurface = DarkGreenGray20, 186 | inverseOnSurface = DarkGreenGray95, 187 | outline = GreenGray50, 188 | ) 189 | 190 | /** 191 | * Dark Android theme color scheme 192 | */ 193 | @VisibleForTesting 194 | val DarkAndroidColorScheme = darkColorScheme( 195 | primary = Green80, 196 | onPrimary = Green20, 197 | primaryContainer = Green30, 198 | onPrimaryContainer = Green90, 199 | secondary = DarkGreen80, 200 | onSecondary = DarkGreen20, 201 | secondaryContainer = DarkGreen30, 202 | onSecondaryContainer = DarkGreen90, 203 | tertiary = Teal80, 204 | onTertiary = Teal20, 205 | tertiaryContainer = Teal30, 206 | onTertiaryContainer = Teal90, 207 | error = Red80, 208 | onError = Red20, 209 | errorContainer = Red30, 210 | onErrorContainer = Red90, 211 | background = DarkGreenGray10, 212 | onBackground = DarkGreenGray90, 213 | surface = DarkGreenGray10, 214 | onSurface = DarkGreenGray90, 215 | surfaceVariant = GreenGray30, 216 | onSurfaceVariant = GreenGray80, 217 | inverseSurface = DarkGreenGray90, 218 | inverseOnSurface = DarkGreenGray10, 219 | outline = GreenGray60, 220 | ) 221 | 222 | /** 223 | * Light Android gradient colors 224 | */ 225 | val LightAndroidGradientColors = GradientColors(container = DarkGreenGray95) 226 | 227 | /** 228 | * Dark Android gradient colors 229 | */ 230 | val DarkAndroidGradientColors = GradientColors(container = Color.Black) 231 | 232 | /** 233 | * Light Android background theme 234 | */ 235 | val LightAndroidBackgroundTheme = BackgroundTheme(color = DarkGreenGray95) 236 | 237 | /** 238 | * Dark Android background theme 239 | */ 240 | val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black) 241 | 242 | /** 243 | * Now in Android theme. 244 | * 245 | * @param darkTheme Whether the theme should use a dark color scheme (follows system by default). 246 | * @param androidTheme Whether the theme should use the Android theme color scheme instead of the 247 | * default theme. 248 | * @param disableDynamicTheming If `true`, disables the use of dynamic theming, even when it is 249 | * supported. This parameter has no effect if [androidTheme] is `true`. 250 | */ 251 | @Composable 252 | fun ComposeTheme( 253 | darkTheme: Boolean = isSystemInDarkTheme(), 254 | androidTheme: Boolean = false, 255 | disableDynamicTheming: Boolean = true, 256 | content: @Composable () -> Unit, 257 | ) { 258 | // Color scheme 259 | val colorScheme = when { 260 | androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme 261 | !disableDynamicTheming && supportsDynamicTheming() -> { 262 | val context = LocalContext.current 263 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 264 | } 265 | 266 | else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme 267 | } 268 | // Gradient colors 269 | val emptyGradientColors = GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp)) 270 | val defaultGradientColors = GradientColors( 271 | top = colorScheme.inverseOnSurface, 272 | bottom = colorScheme.primaryContainer, 273 | container = colorScheme.surface, 274 | ) 275 | val gradientColors = when { 276 | androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors 277 | !disableDynamicTheming && supportsDynamicTheming() -> emptyGradientColors 278 | else -> defaultGradientColors 279 | } 280 | // Background theme 281 | val defaultBackgroundTheme = BackgroundTheme( 282 | color = colorScheme.surface, 283 | tonalElevation = 2.dp, 284 | ) 285 | val backgroundTheme = when { 286 | androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme 287 | else -> defaultBackgroundTheme 288 | } 289 | val tintTheme = when { 290 | androidTheme -> TintTheme() 291 | !disableDynamicTheming && supportsDynamicTheming() -> TintTheme(colorScheme.primary) 292 | else -> TintTheme() 293 | } 294 | // Composition locals 295 | CompositionLocalProvider( 296 | LocalGradientColors provides gradientColors, 297 | LocalBackgroundTheme provides backgroundTheme, 298 | LocalTintTheme provides tintTheme, 299 | ) { 300 | MaterialTheme( 301 | colorScheme = colorScheme, 302 | typography = Typography, 303 | content = content, 304 | ) 305 | } 306 | } 307 | 308 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) 309 | fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 310 | -------------------------------------------------------------------------------- /foundation/ui/src/main/java/com/masewsg/app/ui/components/typography/Typography.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by nphau on 4/10/24, 8:31 PM 3 | * Copyright (c) 2024 . All rights reserved. 4 | * Last modified 4/10/24, 8:31 PM 5 | */ 6 | 7 | package com.masewsg.app.ui.components.typography 8 | 9 | import androidx.compose.material3.Typography 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.text.style.LineHeightStyle 13 | import androidx.compose.ui.text.style.LineHeightStyle.Alignment 14 | import androidx.compose.ui.text.style.LineHeightStyle.Trim 15 | import androidx.compose.ui.unit.sp 16 | 17 | /** 18 | * Now in Android typography. 19 | */ 20 | internal val Typography = Typography( 21 | displayLarge = TextStyle( 22 | fontWeight = FontWeight.Normal, 23 | fontSize = 57.sp, 24 | lineHeight = 64.sp, 25 | letterSpacing = (-0.25).sp, 26 | ), 27 | displayMedium = TextStyle( 28 | fontWeight = FontWeight.Normal, 29 | fontSize = 45.sp, 30 | lineHeight = 52.sp, 31 | letterSpacing = 0.sp, 32 | ), 33 | displaySmall = TextStyle( 34 | fontWeight = FontWeight.Normal, 35 | fontSize = 36.sp, 36 | lineHeight = 44.sp, 37 | letterSpacing = 0.sp, 38 | ), 39 | headlineLarge = TextStyle( 40 | fontWeight = FontWeight.Normal, 41 | fontSize = 32.sp, 42 | lineHeight = 40.sp, 43 | letterSpacing = 0.sp, 44 | ), 45 | headlineMedium = TextStyle( 46 | fontWeight = FontWeight.Normal, 47 | fontSize = 28.sp, 48 | lineHeight = 36.sp, 49 | letterSpacing = 0.sp, 50 | ), 51 | headlineSmall = TextStyle( 52 | fontWeight = FontWeight.Normal, 53 | fontSize = 24.sp, 54 | lineHeight = 32.sp, 55 | letterSpacing = 0.sp, 56 | lineHeightStyle = LineHeightStyle( 57 | alignment = Alignment.Bottom, 58 | trim = Trim.None, 59 | ), 60 | ), 61 | titleLarge = TextStyle( 62 | fontWeight = FontWeight.Bold, 63 | fontSize = 22.sp, 64 | lineHeight = 28.sp, 65 | letterSpacing = 0.sp, 66 | lineHeightStyle = LineHeightStyle( 67 | alignment = Alignment.Bottom, 68 | trim = Trim.LastLineBottom, 69 | ), 70 | ), 71 | titleMedium = TextStyle( 72 | fontWeight = FontWeight.Bold, 73 | fontSize = 18.sp, 74 | lineHeight = 24.sp, 75 | letterSpacing = 0.1.sp, 76 | ), 77 | titleSmall = TextStyle( 78 | fontWeight = FontWeight.Medium, 79 | fontSize = 14.sp, 80 | lineHeight = 20.sp, 81 | letterSpacing = 0.1.sp, 82 | ), 83 | // Default text style 84 | bodyLarge = TextStyle( 85 | fontWeight = FontWeight.Normal, 86 | fontSize = 16.sp, 87 | lineHeight = 24.sp, 88 | letterSpacing = 0.5.sp, 89 | lineHeightStyle = LineHeightStyle( 90 | alignment = Alignment.Center, 91 | trim = Trim.None, 92 | ), 93 | ), 94 | bodyMedium = TextStyle( 95 | fontWeight = FontWeight.Normal, 96 | fontSize = 14.sp, 97 | lineHeight = 20.sp, 98 | letterSpacing = 0.25.sp, 99 | ), 100 | bodySmall = TextStyle( 101 | fontWeight = FontWeight.Normal, 102 | fontSize = 12.sp, 103 | lineHeight = 16.sp, 104 | letterSpacing = 0.4.sp, 105 | ), 106 | // Used for Button 107 | labelLarge = TextStyle( 108 | fontWeight = FontWeight.Medium, 109 | fontSize = 14.sp, 110 | lineHeight = 20.sp, 111 | letterSpacing = 0.1.sp, 112 | lineHeightStyle = LineHeightStyle( 113 | alignment = Alignment.Center, 114 | trim = Trim.LastLineBottom, 115 | ), 116 | ), 117 | // Used for Navigation items 118 | labelMedium = TextStyle( 119 | fontWeight = FontWeight.Medium, 120 | fontSize = 12.sp, 121 | lineHeight = 16.sp, 122 | letterSpacing = 0.5.sp, 123 | lineHeightStyle = LineHeightStyle( 124 | alignment = Alignment.Center, 125 | trim = Trim.LastLineBottom, 126 | ), 127 | ), 128 | // Used for Tag 129 | labelSmall = TextStyle( 130 | fontWeight = FontWeight.Medium, 131 | fontSize = 10.sp, 132 | lineHeight = 14.sp, 133 | letterSpacing = 0.sp, 134 | lineHeightStyle = LineHeightStyle( 135 | alignment = Alignment.Center, 136 | trim = Trim.LastLineBottom, 137 | ), 138 | ), 139 | ) -------------------------------------------------------------------------------- /foundation/ui/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |