Android App MVP for Manager API Access
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:
-
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)
-
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
- Server connection string:
- 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):
- Show Login/Register dialog
- Login tab:
- Username field (OutlinedTextField)
- Password field (OutlinedTextField, visualTransformation = PasswordVisualTransformation())
- "Login" Button
- TextButton to switch to "Register" tab
- Register tab:
- Username field
- Email field (optional)
- Password field (masked)
- "Register" Button
- TextButton to switch to "Login" tab
- Auto-include SSH fingerprint from current SSH key
- 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
- Clone repository
- Open Android Studio
- Select "Open an Existing Project"
- Navigate to
nocodo/android-appdirectory - Click "OK"
- 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:
- Tools → Device Manager
- Create Virtual Device (e.g., Pixel 6, API 34)
- Click "Run" (Shift+F10)
Command Line:
./gradlew installDebug
adb shell am start -n com.nocodo.manager/.MainActivity
Running on Physical Device
- Enable Developer Options on Android device
- Enable USB Debugging
- Connect device via USB
- 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:
- SSH Library: SSHJ - acceptable? Any preference for JSch or Apache MINA SSHD?
- Jetpack Compose - confirmed for UI implementation?
- Minimum API Level: API 26 (Android 8.0) - acceptable? Or support older versions?
- SSH Key Management: Auto-generate if not found - acceptable? Or require user to import?
- Background Service: Use Foreground Service for SSH tunnel - acceptable? Battery implications?
- Material Design version: Material 3 (Material You) - confirmed?
- Testing: Unit tests + Instrumented tests expected? Or just manual testing for MVP?
- Logging: Use Timber or similar logging library?
- Navigation: Single Activity with Navigation Component - confirmed?
- Dependency Injection: Use Hilt - confirmed? Or prefer Koin?
- Package name:
com.nocodo.manager- acceptable? Or different package name? - 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_modelscrate - 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