diff --git a/.github/workflows/android-kotlin-ci.yml b/.github/workflows/android-kotlin-ci.yml
new file mode 100644
index 0000000..3ac4d76
--- /dev/null
+++ b/.github/workflows/android-kotlin-ci.yml
@@ -0,0 +1,47 @@
+name: Android Kotlin CI
+
+on:
+ push:
+ paths:
+ - 'mobile/kotlin-app/**'
+ - '.github/workflows/android-kotlin-ci.yml'
+ pull_request:
+ paths:
+ - 'mobile/kotlin-app/**'
+ - '.github/workflows/android-kotlin-ci.yml'
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: mobile/kotlin-app
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+ cache: gradle
+
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v4
+
+ - name: Build debug APK
+ run: ./gradlew clean assembleDebug
+
+ - name: Build release APK
+ run: ./gradlew assembleRelease
+
+ - name: Upload APK artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: kotlin-apk
+ path: |
+ mobile/kotlin-app/app/build/outputs/apk/debug/*.apk
+ mobile/kotlin-app/app/build/outputs/apk/release/*.apk
diff --git a/mobile/kotlin-app/README.md b/mobile/kotlin-app/README.md
index d3320ff..2594399 100644
--- a/mobile/kotlin-app/README.md
+++ b/mobile/kotlin-app/README.md
@@ -1,16 +1,18 @@
-# Kotlin Android wrapper app
+# Kotlin Android app (mobile-optimized web wrapper)
-This is a native Kotlin Android app that replicates the existing Sunset web project on phone by loading the deployed web app in a full-screen `WebView`.
+This Android app preserves the same Sunset product idea by loading the existing web experience, but adds mobile-first navigation and interaction patterns:
-## Steps
+- Bottom tab navigation to key sections (Home, Courses, About, AI Lab)
+- Pull-to-refresh support
+- Top loading progress indicator
+- Tuned WebView settings and custom Android user-agent suffix
-1. Set your production URL in `app/build.gradle.kts`:
- - `WEB_APP_URL = "https://YOUR_DEPLOYED_WEB_APP_URL"`
+## Setup
+
+1. Set `WEB_APP_URL` in `app/build.gradle.kts` to your deployed site URL.
2. Open `mobile/kotlin-app` in Android Studio.
3. Build and run.
-## Why this approach
+## CI
-- Reuses your existing Next.js codebase.
-- Fastest way to get parity between web and Android.
-- Keeps one product surface while still shipping through Play Store.
+A GitHub Actions workflow is included at `.github/workflows/android-kotlin-ci.yml` to build debug and release APKs automatically on pushes/PRs that touch the Kotlin Android app.
diff --git a/mobile/kotlin-app/app/build.gradle.kts b/mobile/kotlin-app/app/build.gradle.kts
index 71304de..5b3ce64 100644
--- a/mobile/kotlin-app/app/build.gradle.kts
+++ b/mobile/kotlin-app/app/build.gradle.kts
@@ -46,4 +46,5 @@ dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
}
diff --git a/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt b/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt
index 94b5915..593a08d 100644
--- a/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt
+++ b/mobile/kotlin-app/app/src/main/java/com/sunset/replica/MainActivity.kt
@@ -1,16 +1,30 @@
package com.sunset.replica
import android.annotation.SuppressLint
+import android.graphics.Bitmap
import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
+import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
+ private lateinit var pullToRefresh: SwipeRefreshLayout
+ private lateinit var loadingIndicator: ProgressBar
+
+ private val pages = mapOf(
+ R.id.nav_home to "",
+ R.id.nav_courses to "/courses",
+ R.id.nav_about to "/about",
+ R.id.nav_ai to "/new-design"
+ )
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -18,23 +32,72 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
- webView.webChromeClient = WebChromeClient()
- webView.webViewClient = object : WebViewClient() {
- override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
- return false
- }
+ pullToRefresh = findViewById(R.id.pullToRefresh)
+ loadingIndicator = findViewById(R.id.loadingIndicator)
+
+ configureWebView()
+ configureBottomNavigation()
+
+ pullToRefresh.setOnRefreshListener {
+ webView.reload()
+ }
+
+ if (savedInstanceState == null) {
+ navigateTo(R.id.nav_home)
+ } else {
+ webView.restoreState(savedInstanceState)
+ }
+ }
+
+ private fun configureBottomNavigation() {
+ val bottomNavigation: BottomNavigationView = findViewById(R.id.bottomNavigation)
+ bottomNavigation.setOnItemSelectedListener { item ->
+ navigateTo(item.itemId)
+ true
+ }
+ }
+
+ private fun navigateTo(navItemId: Int) {
+ val path = pages[navItemId] ?: ""
+ val destination = "${BuildConfig.WEB_APP_URL.trimEnd('/')}$path"
+ if (webView.url != destination) {
+ webView.loadUrl(destination)
}
+ }
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun configureWebView() {
with(webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
+ cacheMode = WebSettings.LOAD_DEFAULT
useWideViewPort = true
loadWithOverviewMode = true
+ builtInZoomControls = false
+ displayZoomControls = false
+ userAgentString = "$userAgentString SunsetAndroid/1.1"
}
- if (savedInstanceState == null) {
- webView.loadUrl(BuildConfig.WEB_APP_URL)
+ webView.webChromeClient = object : WebChromeClient() {
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ loadingIndicator.progress = newProgress
+ loadingIndicator.visibility = if (newProgress < 100) ProgressBar.VISIBLE else ProgressBar.GONE
+ }
+ }
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean = false
+
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ super.onPageStarted(view, url, favicon)
+ pullToRefresh.isRefreshing = true
+ }
+
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ pullToRefresh.isRefreshing = false
+ }
}
}
@@ -43,16 +106,8 @@ class MainActivity : AppCompatActivity() {
super.onSaveInstanceState(outState)
}
- override fun onRestoreInstanceState(savedInstanceState: Bundle) {
- super.onRestoreInstanceState(savedInstanceState)
- webView.restoreState(savedInstanceState)
- }
-
+ @Deprecated("Deprecated in Java")
override fun onBackPressed() {
- if (webView.canGoBack()) {
- webView.goBack()
- } else {
- super.onBackPressed()
- }
+ if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
}
}
diff --git a/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml b/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml
index 08ec4b5..9acc04b 100644
--- a/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml
+++ b/mobile/kotlin-app/app/src/main/res/layout/activity_main.xml
@@ -1,11 +1,34 @@
-