diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..6321c42 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,31 @@ +name: Android CI +on: + push: + branches: [ main, mvp-android ] + paths: + - 'android/**' + pull_request: + branches: [ main ] + paths: + - 'android/**' + +jobs: + android-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Build and Test + run: | + cd android + chmod +x gradlew + ./gradlew build test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 82f9275..b9af20a 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,75 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Android +*.iml +.gradle/ +local.properties +.idea/ +.DS_Store +build/ +captures/ +.externalNativeBuild +.cxx + +# Keystore files +*.jks +*.keystore +!android/keystore/debug.keystore + +# Android Studio +*.iws +*.ipr +.idea/ +out/ + +# Gradle +.gradle/ +build/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache + +# Generated files +bin/ +gen/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches + +# VS Code +.vscode/ + +# Sonar +.sonar/ +.scannerwork/ + +# Test reports +app/build/reports/ +app/build/test-results/ + +# Detekt +build/reports/detekt/ + +# KtLint +build/reports/ktlint/ +.gradle/ + +# Android Studio +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index dde1e6d..758e59b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ # RunOn +## Project Status + [![Build and Test](https://github.com/fleXRPL/RunOn/actions/workflows/build.yml/badge.svg)](https://github.com/fleXRPL/RunOn/actions/workflows/build.yml) +[![Android CI](https://github.com/fleXRPL/RunOn/actions/workflows/android.yml/badge.svg)](https://github.com/fleXRPL/RunOn/actions/workflows/android.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=fleXRPL_RunOn&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=fleXRPL_RunOn) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=fleXRPL_RunOn&metric=coverage)](https://sonarcloud.io/summary/new_code?id=fleXRPL_RunOn) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=fleXRPL_RunOn&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=fleXRPL_RunOn) @@ -24,6 +27,7 @@ RunOn helps runners find and participate in local running events by providing: ## Architecture ### Backend + - **APIs**: - Google Search API for event discovery - Google Calendar API for event management @@ -33,6 +37,7 @@ RunOn helps runners find and participate in local running events by providing: - SonarCloud quality gates ### Android App + - **Language**: Kotlin - **UI Framework**: Jetpack Compose - **Key Features**: @@ -40,23 +45,50 @@ RunOn helps runners find and participate in local running events by providing: - Direct Google Calendar integration - Google Sign-in +## Project Structure + +```bash +RunOn/ +├── .github/ +│ └── workflows/ +│ └── build.yml # CI/CD pipeline +│ └── android.yml # Android CI/CD pipeline +├── android/ # Android mobile app +│ ├── app/ # Main Android application +│ └── docs/ # Android documentation +├── backend/ # Core functionality +│ ├── functions/ # Business logic +│ ├── models/ # Data models +│ ├── tests/ # Test suite +│ └── scripts/ # Development tools +``` + ## Development Setup -1. Clone the repository: +### Android Setup + ```bash +# Clone repository git clone https://github.com/fleXRPL/RunOn.git -cd RunOn +cd RunOn/android + +# Run Android setup script +chmod +x scripts/setup.sh +bash scripts/setup.sh ``` -2. Install dependencies: +### Backend Setup + ```bash -cd backend +# Clone repository +git clone https://github.com/fleXRPL/RunOn.git +cd RunOn/backend + +# Install dependencies pip install -r requirements.txt pip install -r requirements-dev.txt -``` -3. Run tests: -```bash +# Run tests and checks bash scripts/format_and_lint.sh ``` @@ -69,7 +101,8 @@ bash scripts/format_and_lint.sh 1. Ensure all tests pass with 100% coverage 2. Follow PEP 8 style guide -3. Run `format_and_lint.sh` before committing +3. Run `format_and_lint.sh` before committing to backend +4. Run `format_and_lint.sh` before committing to android ## License diff --git a/android/README.md b/android/README.md index 9e0a587..146f285 100644 --- a/android/README.md +++ b/android/README.md @@ -1,103 +1,84 @@ -# RunOn Android Application +# RunOn! Android App -## Overview +## MVP Features -The RunOn Android application provides a native mobile interface for discovering and managing running events, with seamless calendar integration and user management features. +- Event discovery integration +- Google Calendar sync +- Clean architecture implementation ## Project Structure -``` +```bash android/ ├── app/ │ ├── src/ │ │ ├── main/ -│ │ │ ├── kotlin/com/flexrpl/runon/ -│ │ │ │ ├── activities/ # Main UI activities -│ │ │ │ ├── fragments/ # UI fragments -│ │ │ │ ├── models/ # Data models -│ │ │ │ ├── network/ # API client and services -│ │ │ │ ├── services/ # Background services -│ │ │ │ └── utils/ # Utility classes -│ │ │ └── res/ # Resources -│ │ └── test/ # Unit tests -│ ├── build.gradle # App-level build config -│ └── proguard-rules.pro # ProGuard rules -├── gradle/ # Gradle wrapper -├── build.gradle # Project-level build config -└── settings.gradle # Project settings +│ │ │ ├── kotlin/ +│ │ │ │ └── com/flexrpl/runon/ +│ │ │ │ ├── data/ # Data layer +│ │ │ │ ├── domain/ # Business logic +│ │ │ │ └── ui/ # Presentation layer +│ │ │ └── res/ # Resources +│ │ └── test/ # Unit tests +│ └── build.gradle.kts +└── build.gradle.kts ``` -## Technology Stack - -- **Language**: Kotlin -- **UI Framework**: Jetpack Compose -- **Architecture**: MVVM with Clean Architecture -- **Dependencies**: - - Android Architecture Components - - Retrofit for API communication - - Room for local storage - - Hilt for dependency injection - - Material Design 3 - ## Development Setup -1. Install Android Studio (latest version) -2. Install JDK 17 or later -3. Clone the repository -4. Open the project in Android Studio -5. Sync Gradle files -6. Run the application - -## Building and Running +### Quick Start ```bash -# Build debug variant -./gradlew assembleDebug - -# Run tests -./gradlew test - -# Install on connected device -./gradlew installDebug +# From project root +./android/scripts/setup.sh ``` -## Testing +The setup script will: + +- Install required tools (via Homebrew) +- Initialize Gradle +- Set up permissions +- Run initial build + +### Required Software + +- Android Studio Hedgehog | 2023.1.1 +- JDK 17 + +## Core Dependencies + +```kotlin +dependencies { + // Core Android + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + + // UI + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2024.01.00")) + + // Testing + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") +} +``` -- Unit Tests: `./gradlew test` -- Instrumentation Tests: `./gradlew connectedAndroidTest` -- UI Tests: `./gradlew connectedCheck` +## Getting Started -## CI/CD +1. Clone the repository +2. Open in Android Studio +3. Sync Gradle files +4. Run tests +5. Build and run -The project uses GitHub Actions for continuous integration and deployment: +## Testing Requirements -- Automated builds -- Unit test execution -- Code quality checks -- Release management +- Unit tests for all business logic +- UI tests for critical paths +- Integration tests for API communication ## Code Style -The project follows the official Kotlin style guide and Android best practices: - -- Kotlin style guide -- Android architecture components patterns -- Material Design guidelines - -## Security - -- SSL pinning for API communication -- Secure storage for user credentials -- ProGuard optimization and obfuscation - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Commit your changes -4. Push to the branch -5. Create a Pull Request - -## License - -This project is licensed under the terms of the [LICENSE](../LICENSE) file in the root directory. +- Follow Kotlin coding conventions +- Use Compose best practices +- Maintain clean architecture separation diff --git a/android/android/app/src/main/kotlin/com/flexrpl/runon/domain/model/Event.kt b/android/android/app/src/main/kotlin/com/flexrpl/runon/domain/model/Event.kt new file mode 100644 index 0000000..666380f --- /dev/null +++ b/android/android/app/src/main/kotlin/com/flexrpl/runon/domain/model/Event.kt @@ -0,0 +1,9 @@ +package com.flexrpl.runon.domain.model + +data class Event( + val id: String, + val title: String, + val description: String, + val date: String, + val location: String +) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..de19b2d --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + id("com.android.application") + kotlin("android") + id("org.jlleitschuh.gradle.ktlint") version "11.6.1" +} + +android { + namespace = "com.flexrpl.runon" + compileSdk = 34 + + defaultConfig { + applicationId = "com.flexrpl.runon" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } + + signingConfigs { + getByName("debug") { + storeFile = file("../keystore/debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + + buildTypes { + debug { + signingConfig = signingConfigs.getByName("debug") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + // Core Android + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + + // Compose (minimal set) + implementation(platform("androidx.compose:compose-bom:2024.01.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui-tooling-preview") + + // Basic Testing + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + + // Debug tooling + debugImplementation("androidx.compose.ui:ui-tooling") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") +} + +// Basic KtLint configuration +ktlint { + android.set(true) + verbose.set(true) +} diff --git a/android/app/config/detekt.yml b/android/app/config/detekt.yml new file mode 100644 index 0000000..31f9b88 --- /dev/null +++ b/android/app/config/detekt.yml @@ -0,0 +1,11 @@ +complexity: + LongMethod: + threshold: 60 + TooManyFunctions: + thresholdInFiles: 20 + thresholdInClasses: 20 + thresholdInInterfaces: 20 + +style: + MaxLineLength: + maxLineLength: 120 \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c318295 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/flexrpl/runon/MainActivity.kt b/android/app/src/main/kotlin/com/flexrpl/runon/MainActivity.kt new file mode 100644 index 0000000..56c4c42 --- /dev/null +++ b/android/app/src/main/kotlin/com/flexrpl/runon/MainActivity.kt @@ -0,0 +1,36 @@ +package com.flexrpl.runon + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.flexrpl.runon.data.repository.EventRepositoryImpl +import com.flexrpl.runon.ui.EventScreen +import com.flexrpl.runon.ui.EventViewModel + +class MainActivity : ComponentActivity() { + private val viewModel by lazy { + val repository = EventRepositoryImpl() + ViewModelProvider(this, EventViewModelFactory(repository))[EventViewModel::class.java] + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + EventScreen(viewModel) + } + } +} + +class EventViewModelFactory( + private val repository: EventRepositoryImpl +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(EventViewModel::class.java)) { + return EventViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/android/app/src/main/kotlin/com/flexrpl/runon/data/repository/EventRepository.kt b/android/app/src/main/kotlin/com/flexrpl/runon/data/repository/EventRepository.kt new file mode 100644 index 0000000..aa6eaaa --- /dev/null +++ b/android/app/src/main/kotlin/com/flexrpl/runon/data/repository/EventRepository.kt @@ -0,0 +1,8 @@ +package com.flexrpl.runon.data.repository + +import com.flexrpl.runon.domain.model.Event +import kotlinx.coroutines.flow.Flow + +interface EventRepository { + fun getEvents(): Flow> +} diff --git a/android/app/src/main/kotlin/com/flexrpl/runon/data/repository/EventRepositoryImpl.kt b/android/app/src/main/kotlin/com/flexrpl/runon/data/repository/EventRepositoryImpl.kt new file mode 100644 index 0000000..b695397 --- /dev/null +++ b/android/app/src/main/kotlin/com/flexrpl/runon/data/repository/EventRepositoryImpl.kt @@ -0,0 +1,14 @@ +package com.flexrpl.runon.data.repository + +import com.flexrpl.runon.domain.model.Event +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class EventRepositoryImpl : EventRepository { + override fun getEvents(): Flow> = flowOf( + listOf( + Event("1", "Morning Run", "5K run in the park", "2024-02-14", "Central Park"), + Event("2", "Evening Jog", "Easy 3K", "2024-02-15", "Riverside") + ) + ) +} diff --git a/android/app/src/main/kotlin/com/flexrpl/runon/domain/model/Event.kt b/android/app/src/main/kotlin/com/flexrpl/runon/domain/model/Event.kt new file mode 100644 index 0000000..666380f --- /dev/null +++ b/android/app/src/main/kotlin/com/flexrpl/runon/domain/model/Event.kt @@ -0,0 +1,9 @@ +package com.flexrpl.runon.domain.model + +data class Event( + val id: String, + val title: String, + val description: String, + val date: String, + val location: String +) diff --git a/android/app/src/main/kotlin/com/flexrpl/runon/ui/EventScreen.kt b/android/app/src/main/kotlin/com/flexrpl/runon/ui/EventScreen.kt new file mode 100644 index 0000000..1b482c1 --- /dev/null +++ b/android/app/src/main/kotlin/com/flexrpl/runon/ui/EventScreen.kt @@ -0,0 +1,58 @@ +package com.flexrpl.runon.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flexrpl.runon.domain.model.Event + +@Composable +fun EventScreen(viewModel: EventViewModel) { + Surface(color = MaterialTheme.colorScheme.background) { + val events by viewModel.events.collectAsState(initial = emptyList()) + EventList(events = events) + } +} + +@Composable +private fun EventList(events: List) { + LazyColumn { + items(events) { event -> + EventCard(event = event) + } + } +} + +@Composable +private fun EventCard(event: Event) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = event.title, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = event.date, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = event.location, + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/android/app/src/main/kotlin/com/flexrpl/runon/ui/EventViewModel.kt b/android/app/src/main/kotlin/com/flexrpl/runon/ui/EventViewModel.kt new file mode 100644 index 0000000..2872fbe --- /dev/null +++ b/android/app/src/main/kotlin/com/flexrpl/runon/ui/EventViewModel.kt @@ -0,0 +1,11 @@ +package com.flexrpl.runon.ui + +import androidx.lifecycle.ViewModel +import com.flexrpl.runon.data.repository.EventRepository +import kotlinx.coroutines.flow.StateFlow + +class EventViewModel( + private val repository: EventRepository +) : ViewModel() { + val events = repository.getEvents() +} diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..5527bf5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap/ic_launcher.xml b/android/app/src/main/res/mipmap/ic_launcher.xml new file mode 100644 index 0000000..f3f7fbe --- /dev/null +++ b/android/app/src/main/res/mipmap/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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 0000000..c7f5192 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #FF6200EE + #FFFFFF + #FFFFFF + \ No newline at end of file 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 0000000..87f2117 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + RunOn + \ No newline at end of file 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 0000000..372780d --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +