nocodo icon indicating copy to clipboard operation
nocodo copied to clipboard

Android App MVP for Manager API Access

Open brainless opened this issue 2 months ago • 0 comments

Android App MVP for Manager API Access

Overview

Create a native Android app using Kotlin to enable accessing the nocodo manager from Android smartphones and tablets. This will be a standard Android Studio project that can be:

  • Opened in Android Studio
  • Built using Gradle
  • Run in Android Emulator
  • Deployed to physical Android devices via USB or wireless debugging
  • Eventually published to Google Play Store

Project Setup

Project Structure

android-app/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/nocodo/manager/  (Kotlin source files)
│   │   │   ├── res/                       (Resources)
│   │   │   └── AndroidManifest.xml
│   │   └── androidTest/                   (Instrumented tests)
│   └── build.gradle.kts                   (App-level Gradle - Kotlin DSL)
├── gradle/
├── build.gradle.kts                       (Project-level Gradle - Kotlin DSL)
├── settings.gradle.kts
├── gradle.properties
└── README.md

Technology Stack

  • Language: Kotlin (100%)
  • Build System: Gradle with Kotlin DSL (.kts)
  • IDE: Android Studio (latest stable version)
  • Min SDK: API 26 (Android 8.0 Oreo)
  • Target SDK: API 34 (Android 14)
  • Compile SDK: API 34

Dependencies (build.gradle.kts)

dependencies {
    // Core Android
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    
    // Jetpack Compose
    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")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation("androidx.navigation:navigation-compose:2.7.6")
    
    // ViewModel & LiveData
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
    
    // Hilt (Dependency Injection)
    implementation("com.google.dagger:hilt-android:2.50")
    kapt("com.google.dagger:hilt-compiler:2.50")
    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
    
    // Room (Database)
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
    
    // Retrofit & OkHttp (HTTP client)
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
    
    // SSHJ (SSH library)
    implementation("com.hierynomus:sshj:0.37.0")
    
    // Security
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
    
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // Testing
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}

Reference Implementation

The desktop-app (Rust/egui) serves as the reference implementation for:

  • SSH tunnel setup and management
  • HTTP API authentication flow
  • UI/UX patterns for projects and servers

Architecture Requirements

Authentication Flow

Based on desktop-app implementation:

  1. SSH Authentication (desktop-app/src/ssh.rs)

    • Use SSH key-based authentication (RSA, ED25519, or ECDSA)
    • Support custom SSH key path or default locations
    • Establish SSH tunnel with port forwarding to remote manager API
    • Default SSH port: 22, configurable
    • Default remote manager port: 8081 (forwarded to localhost)
  2. HTTP API Authentication (desktop-app/src/api_client.rs, desktop-app/src/connection_manager.rs)

    • After SSH tunnel is established, connect to manager API via forwarded port
    • Support both Register and Login flows
    • Registration requires: username, password, email (optional), SSH public key, SSH fingerprint
    • Login requires: username, password, SSH fingerprint
    • Both flows return JWT token for subsequent API calls
    • Store JWT token and include in Authorization header: Bearer <token>

Data Models (Kotlin)

Reference: manager_models crate (used by desktop-app)

Project Model:

data class Project(
    val id: Long,
    val name: String,
    val path: String,
    val description: String? = null
)

Server Model (Room Entity):

@Entity(tableName = "servers")
data class Server(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val host: String,
    val user: String,
    val keyPath: String? = null,
    val port: Int = 22
)

API Endpoints

Base URL: http://localhost:<forwarded_port>

  • Health check: GET /api/health
  • Register: POST /api/auth/register
  • Login: POST /api/auth/login
  • List projects: GET /api/projects (requires JWT)
  • List servers: Stored locally in Room database

API Client Interface (Retrofit)

interface ManagerApiService {
    @GET("/api/health")
    suspend fun healthCheck(): Response<ServerStatus>
    
    @POST("/api/auth/register")
    suspend fun register(@Body request: RegisterRequest): Response<UserResponse>
    
    @POST("/api/auth/login")
    suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
    
    @GET("/api/projects")
    suspend fun listProjects(): Response<ProjectListResponse>
}

MVP Features

1. Navigation (Burger Menu)

Implementation: NavigationDrawer with Jetpack Compose

  • Menu Items: ["Projects", "Servers"]
  • Default page: Servers (matches desktop-app behavior)

2. Projects Page

Requirements:

  • Display list of projects as cards (reference: desktop-app/src/pages/projects.rs)
  • Each card shows:
    • Project name (title)
    • Project path (subtitle, muted color)
    • Description (if present)
  • Compose Implementation: LazyVerticalGrid with Card components
  • Material 3 card with 8.dp corner radius, 12.dp padding
  • Responsive grid layout with spacing
  • No project details page in MVP
  • Refresh projects list after successful authentication

States to handle (Kotlin sealed class):

sealed class ProjectsUiState {
    object Disconnected : ProjectsUiState()
    object Connecting : ProjectsUiState()
    object Loading : ProjectsUiState()
    object Empty : ProjectsUiState()
    data class Success(val projects: List<Project>) : ProjectsUiState()
    data class Error(val message: String) : ProjectsUiState()
}

3. Servers Page

Requirements:

  • NO local server section (SSH servers only)
  • Show list of saved servers (reference: desktop-app/src/pages/servers.rs)
  • Each server entry shows:
    • Server connection string: user@host:port
    • SSH key path (or "Default" if using default keys)
    • "Connect" button
  • CTA at top: FloatingActionButton (FAB) "Connect to New Server"

Compose Implementation: LazyColumn with Card items + FAB

Saved servers storage:

  • Use Room database
  • DAO interface:
@Dao
interface ServerDao {
    @Query("SELECT * FROM servers")
    fun getAllServers(): Flow<List<Server>>
    
    @Insert
    suspend fun insertServer(server: Server)
    
    @Delete
    suspend fun deleteServer(server: Server)
}

4. Connect to New Server Dialog

Implementation: Compose Dialog with TextFields

Fields (reference: desktop-app/src/components/connection_dialog.rs):

  • SSH Server (hostname/IP) - OutlinedTextField
  • Username - OutlinedTextField
  • Port (default: 22) - OutlinedTextField with number keyboard
  • SSH Key Path (optional) - OutlinedTextField with file picker button

Additional Android-specific feature:

  • Display current user's default SSH public key in a copyable OutlinedTextField (read-only)
  • Show fingerprint (SHA256) below the public key
  • "Copy" IconButton to copy public key to clipboard (using ClipboardManager)
  • If no SSH key exists, generate a default SSH key pair using SSHJ

Default SSH key locations (Android):

  • Check: <app_private_storage>/.ssh/id_ed25519, id_rsa, id_ecdsa
  • Generate if none found: ED25519 key pair

SSH Key Generation (Kotlin/SSHJ):

fun generateSshKeyPair(context: Context) {
    val sshDir = File(context.filesDir, ".ssh")
    sshDir.mkdirs()
    val keyFile = File(sshDir, "id_ed25519")
    
    val generator = KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider())
    val keyPair = generator.generateKeyPair()
    
    // Save private key
    // Save public key
}

5. Authentication Flow After SSH Connection

Implementation: Compose Dialog with TabRow

After SSH tunnel is successfully established (reference: desktop-app/src/components/auth_dialog.rs):

  1. Show Login/Register dialog
  2. Login tab:
    • Username field (OutlinedTextField)
    • Password field (OutlinedTextField, visualTransformation = PasswordVisualTransformation())
    • "Login" Button
    • TextButton to switch to "Register" tab
  3. Register tab:
    • Username field
    • Email field (optional)
    • Password field (masked)
    • "Register" Button
    • TextButton to switch to "Login" tab
  4. Auto-include SSH fingerprint from current SSH key
  5. On successful authentication:
    • Store JWT token in EncryptedSharedPreferences
    • Navigate to Projects screen using NavController
    • Trigger projects list refresh via ViewModel

6. Connection Management

Implementation: Android Foreground Service

Requirements (reference: desktop-app/src/connection_manager.rs):

  • Maintain SSH tunnel in SshConnectionService (Foreground Service)
  • Show persistent notification: "Connected to "
  • Periodic health checks (GET /api/health) every 30 seconds using WorkManager or coroutine
  • Auto-reconnect on connection failure (max 2 attempts)
  • Show 401 errors as "Authentication Required" and prompt for login
  • Handle connection states using StateFlow:
sealed class ConnectionState {
    object Disconnected : ConnectionState()
    object Connecting : ConnectionState()
    data class Connected(val serverHost: String) : ConnectionState()
    data class Error(val message: String) : ConnectionState()
}

Service Implementation:

class SshConnectionService : Service() {
    private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
    val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // Create notification channel
        // Show foreground notification
        // Establish SSH tunnel in coroutine
        return START_STICKY
    }
}

Development Workflow

Opening in Android Studio

  1. Clone repository
  2. Open Android Studio
  3. Select "Open an Existing Project"
  4. Navigate to nocodo/android-app directory
  5. Click "OK"
  6. Wait for Gradle sync to complete

Building the App

Command Line:

cd android-app
./gradlew assembleDebug  # Debug build
./gradlew assembleRelease  # Release build

Android Studio:

  • Build → Make Project (Ctrl+F9)
  • Build → Build Bundle(s) / APK(s) → Build APK(s)

Running in Emulator

Android Studio:

  1. Tools → Device Manager
  2. Create Virtual Device (e.g., Pixel 6, API 34)
  3. Click "Run" (Shift+F10)

Command Line:

./gradlew installDebug
adb shell am start -n com.nocodo.manager/.MainActivity

Running on Physical Device

  1. Enable Developer Options on Android device
  2. Enable USB Debugging
  3. Connect device via USB
  4. Click "Run" in Android Studio, select physical device

Testing

Unit Tests:

./gradlew test

Instrumented Tests (Emulator/Device):

./gradlew connectedAndroidTest

Project File Organization

Package Structure

com.nocodo.manager/
├── ui/
│   ├── screens/
│   │   ├── projects/
│   │   │   ├── ProjectsScreen.kt
│   │   │   └── ProjectsViewModel.kt
│   │   ├── servers/
│   │   │   ├── ServersScreen.kt
│   │   │   └── ServersViewModel.kt
│   │   └── MainActivity.kt
│   ├── components/
│   │   ├── ConnectionDialog.kt
│   │   └── AuthDialog.kt
│   └── theme/
│       ├── Theme.kt
│       ├── Color.kt
│       └── Type.kt
├── data/
│   ├── local/
│   │   ├── AppDatabase.kt
│   │   ├── ServerDao.kt
│   │   └── entities/
│   ├── remote/
│   │   ├── ManagerApiService.kt
│   │   ├── dto/  (Data Transfer Objects)
│   │   └── interceptors/
│   └── repository/
│       ├── ServerRepository.kt
│       └── ProjectRepository.kt
├── domain/
│   ├── model/
│   │   ├── Project.kt
│   │   └── Server.kt
│   └── usecase/
├── service/
│   └── SshConnectionService.kt
├── ssh/
│   ├── SshManager.kt
│   └── SshKeyManager.kt
└── di/
    ├── AppModule.kt
    ├── DatabaseModule.kt
    └── NetworkModule.kt

Questions for Clarification

Please review and update these default answers as needed:

  1. SSH Library: SSHJ - acceptable? Any preference for JSch or Apache MINA SSHD?
  2. Jetpack Compose - confirmed for UI implementation?
  3. Minimum API Level: API 26 (Android 8.0) - acceptable? Or support older versions?
  4. SSH Key Management: Auto-generate if not found - acceptable? Or require user to import?
  5. Background Service: Use Foreground Service for SSH tunnel - acceptable? Battery implications?
  6. Material Design version: Material 3 (Material You) - confirmed?
  7. Testing: Unit tests + Instrumented tests expected? Or just manual testing for MVP?
  8. Logging: Use Timber or similar logging library?
  9. Navigation: Single Activity with Navigation Component - confirmed?
  10. Dependency Injection: Use Hilt - confirmed? Or prefer Koin?
  11. Package name: com.nocodo.manager - acceptable? Or different package name?
  12. Application ID: com.nocodo.manager - acceptable for Google Play?

Acceptance Criteria

  • [ ] Android Studio project opens without errors
  • [ ] App builds successfully with ./gradlew assembleDebug
  • [ ] App runs in Android Emulator (API 26+)
  • [ ] App runs on physical Android device
  • [ ] User can add a new server via "Connect to New Server" FAB
  • [ ] SSH public key is displayed and copyable to clipboard
  • [ ] SSH connection establishes successfully to remote server
  • [ ] Login/Register dialog appears after SSH connection
  • [ ] User can register a new account
  • [ ] User can login with existing credentials
  • [ ] JWT token is stored securely in EncryptedSharedPreferences
  • [ ] Projects list loads and displays after authentication using LazyVerticalGrid
  • [ ] Server list shows saved servers from Room database
  • [ ] User can connect to saved server from list
  • [ ] Navigation drawer navigates between Projects and Servers screens
  • [ ] SSH tunnel persists in background (Foreground Service with notification)
  • [ ] App auto-reconnects on connection loss
  • [ ] 401 errors trigger login dialog
  • [ ] All error states display appropriate Snackbar or AlertDialog messages
  • [ ] App follows Material 3 design guidelines
  • [ ] No memory leaks (verified with LeakCanary in debug builds)

References

  • Desktop app implementation: /desktop-app/src/
  • Manager API models: manager_models crate
  • Manager API endpoints: manager/src/handlers.rs
  • Android Developer Guides: https://developer.android.com/
  • Jetpack Compose: https://developer.android.com/jetpack/compose
  • SSHJ Documentation: https://github.com/hierynomus/sshj

brainless avatar Nov 08 '25 13:11 brainless