diff --git a/.gitignore b/.gitignore index ce30d749e..08783ba40 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,12 @@ tasks/ .vscode/ .idea/ +# Android +*.apk +*.aab +*.keystore +*.jks +local.properties + # Added by goreleaser init: dist/ diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..56ce17a7a --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/app/build +/feature/*/build +/core/*/build diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 000000000..9149c4fd0 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.serialization) +} + +android { + namespace = "io.picoclaw.android" + compileSdk = 35 + + defaultConfig { + applicationId = "io.picoclaw.android" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation(project(":feature:chat")) + implementation(project(":core:domain")) + implementation(project(":core:data")) + implementation(project(":core:ui")) + + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.material3) + implementation(libs.activity.compose) + implementation(libs.core.ktx) + implementation(libs.lifecycle.runtime.compose) + + implementation(libs.koin.android) + implementation(libs.koin.compose) + + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.websockets) + + implementation(libs.room.runtime) + implementation(libs.room.ktx) + + implementation(libs.coroutines.android) + + debugImplementation(libs.compose.ui.tooling) +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..226da85ae --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,6 @@ +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt +-keepclassmembers class kotlinx.serialization.json.** { *** Companion; } +-keepclasseswithmembers class io.picoclaw.android.** { + kotlinx.serialization.KSerializer serializer(...); +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6a6ba1bf9 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/io/picoclaw/android/MainActivity.kt b/android/app/src/main/java/io/picoclaw/android/MainActivity.kt new file mode 100644 index 000000000..8d1857617 --- /dev/null +++ b/android/app/src/main/java/io/picoclaw/android/MainActivity.kt @@ -0,0 +1,20 @@ +package io.picoclaw.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import io.picoclaw.android.core.ui.theme.PicoClawTheme +import io.picoclaw.android.feature.chat.screen.ChatScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PicoClawTheme { + ChatScreen() + } + } + } +} diff --git a/android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt b/android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt new file mode 100644 index 000000000..b4d3be91e --- /dev/null +++ b/android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt @@ -0,0 +1,16 @@ +package io.picoclaw.android + +import android.app.Application +import io.picoclaw.android.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class PicoClawApp : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@PicoClawApp) + modules(appModule) + } + } +} diff --git a/android/app/src/main/java/io/picoclaw/android/di/AppModule.kt b/android/app/src/main/java/io/picoclaw/android/di/AppModule.kt new file mode 100644 index 000000000..9ac3be9b0 --- /dev/null +++ b/android/app/src/main/java/io/picoclaw/android/di/AppModule.kt @@ -0,0 +1,65 @@ +package io.picoclaw.android.di + +import androidx.room.Room +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.websocket.WebSockets +import io.picoclaw.android.core.data.local.AppDatabase +import io.picoclaw.android.core.data.remote.WebSocketClient +import io.picoclaw.android.core.data.repository.ChatRepositoryImpl +import io.picoclaw.android.core.domain.repository.ChatRepository +import io.picoclaw.android.core.domain.usecase.LoadMoreMessagesUseCase +import io.picoclaw.android.core.domain.usecase.ObserveConnectionUseCase +import io.picoclaw.android.core.domain.usecase.ObserveMessagesUseCase +import io.picoclaw.android.core.domain.usecase.SendMessageUseCase +import io.picoclaw.android.feature.chat.ChatViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import okhttp3.OkHttpClient +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module +import java.util.concurrent.TimeUnit + +val appModule = module { + // CoroutineScope + single { CoroutineScope(SupervisorJob() + Dispatchers.IO) } + + // Room + single { + Room.databaseBuilder( + androidContext(), + AppDatabase::class.java, + "picoclaw.db" + ).build() + } + single { get().messageDao() } + + // Ktor HttpClient + single { + HttpClient(OkHttp) { + install(WebSockets) + engine { + preconfigured = OkHttpClient.Builder() + .pingInterval(30, TimeUnit.SECONDS) + .build() + } + } + } + + // WebSocketClient + single { WebSocketClient(get()) } + + // Repository + single { ChatRepositoryImpl(get(), get(), get()) } + + // UseCases + factory { SendMessageUseCase(get()) } + factory { ObserveMessagesUseCase(get()) } + factory { ObserveConnectionUseCase(get()) } + factory { LoadMoreMessagesUseCase(get()) } + + // ViewModel + viewModel { ChatViewModel(get(), get(), get(), get(), get()) } +} diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..526d293ac --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..768b058a6 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + diff --git a/android/app/src/main/res/values/ic_launcher_colors.xml b/android/app/src/main/res/values/ic_launcher_colors.xml new file mode 100644 index 000000000..e8bc98809 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_colors.xml @@ -0,0 +1,5 @@ + + + #2196F3 + #FFFFFF + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..9652097f9 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + PicoClaw + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..92c14c2ad --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +