Skip to content

feat(android): Android mobile platform support#2988

Open
StarbirdTech wants to merge 23 commits intospacedriveapp:mainfrom
StarbirdTech:feat/android-mobile-support
Open

feat(android): Android mobile platform support#2988
StarbirdTech wants to merge 23 commits intospacedriveapp:mainfrom
StarbirdTech:feat/android-mobile-support

Conversation

@StarbirdTech
Copy link
Contributor

@StarbirdTech StarbirdTech commented Jan 21, 2026

Adds native Android support to Spacedrive's mobile app. This PR covers Android platform detection in the Rust core, a Kotlin native module with JNI bindings, platform-specific UI with Material Design 3 styling, and production infrastructure (CI, security, signing).

Non-Android-specific changes were extracted into separate PRs to keep this diff focused:

What this adds

  • Native Android volume detection, device identification, and SAF path resolution
  • Kotlin native module with JNI bindings for the Spacedrive core
  • Platform-specific tab navigation — M3 pill indicator on Android, liquid glass on iOS
  • M3 collapsing header on the Android overview screen with storage picker
  • Storage permission flow with user prompts and banner component
  • Mobile transport layer with retry logic, health checks, and subscription cleanup
  • Android CI jobs (lint, Rust tests, detekt/ktlint static analysis)
  • Release signing config with environment variables and dynamic versionCode

Core Rust (Android platform support)

  • core/src/volume/platform/android.rs — Android volume detection (359 lines)
  • core/src/volume/platform/mod.rs, core/src/volume/detection.rs, core/src/volume/backend/local.rs — Android integration
  • core/src/domain/device.rs — skip sysinfo on Android/iOS (SELinux crash fix)
  • core/src/ops/locations/add/action.rssafe_canonicalize() for Android paths + permission checks
  • core/src/library/{mod,manager}.rs — lock-free library ID access for mobile reliability

Android build & config

  • All apps/mobile/android/ files (manifest, gradle, detekt, signing docs, backup rules)
  • apps/mobile/modules/sd-mobile-core/android/ (native Kotlin module, build scripts)
  • apps/mobile/modules/sd-mobile-core/core/src/lib.rs — JNI bindings
  • .github/workflows/ci.yml — Android lint/test CI jobs

Mobile frontend

  • apps/mobile/src/app/(drawer)/(tabs)/_layout.tsx — platform-specific tab navigation
  • apps/mobile/src/screens/overview/OverviewScreen.tsx — M3 collapsing header + storage picker
  • apps/mobile/src/client/ — transport, client, subscription improvements
  • apps/mobile/src/hooks/useStoragePermission.ts + StoragePermissionBanner.tsx
  • Browse screen component updates

iOS side-effects

  • apps/mobile/ios/Podfile.lock — updated deps
  • apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj

Security

  • Disabled Android backup to protect user data
  • FFI null pointer validation and panic prevention
  • Thread-safe listener counters and promise handling
  • Path traversal validation for SAF URIs

Stats

21 commits, 47 files, +4,267 / -679

@StarbirdTech StarbirdTech marked this pull request as ready for review January 21, 2026 05:14
StarbirdTech and others added 21 commits February 6, 2026 03:42
- Add @types/react and @types/react-dom overrides to root package.json
- Update packages/assets exports for icons, images, svgs, sounds, videos, lottie
- Add additional ts-client exports for hooks and generated types
- Move react and @tanstack/react-query to devDependencies in ts-client
- Optimize metro.config.js watch folders to avoid watching Rust target/ dirs
- Resolve React from workspace root (bun hoists it there)
- Update bun.lockb

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add packages/assets/types.d.ts for @sd/assets module declarations
  - icons, images (png/jpg), svgs, videos, sounds, lottie
- Add apps/mobile/src/types/assets.d.ts for React Native asset imports
- Add apps/mobile/src/types/expo-router.d.ts for unstable native tabs types
  - Augments expo-router with missing 'name' prop on NativeTabs.Trigger

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tection

Device detection:
- Skip sysinfo on mobile platforms (was causing crashes on Android)
- Return minimal SystemInfo with architecture and form factor
- TODO: Implement with native APIs for richer device info

Volume detection:
- Add Android volume detection module (core/src/volume/platform/android.rs)
- Detect app storage, external storage (/storage/emulated/0), SD cards, USB drives
- Use statvfs for capacity queries (Android is Linux-based)
- Read device model from /system/build.prop
- Check /proc/mounts and /sys/block for removable storage detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Library:
- Store library ID separately for lock-free access
- Avoids potential panic when reading config under contention

Location add:
- Add safe_canonicalize() that handles Android filesystem edge cases
- Falls back to partial canonicalization or raw path if full resolution fails
- Add read permission verification before adding location
- Add defensive device existence check with race condition documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…error handling

Kotlin module (SDMobileCoreModule):
- Add pickFolder() using Storage Access Framework (ACTION_OPEN_DOCUMENT_TREE)
- Add getPathFromUri() to convert content:// URIs to filesystem paths
- Handle primary storage, home directory, and external volumes (SD cards, USB)
- Use StorageManager API on Android N+ for reliable path resolution

Rust FFI (lib.rs):
- Add safe_cstring() to prevent panics from null bytes in strings
- Add structured JSON-RPC error responses with error_type and details
- Add library ID validation before processing requests
- Initialize Android logger for logcat visibility
- Wrap JNI callbacks in catch_unwind to prevent panics crossing FFI boundary
- Add proper error handling for JavaVM attachment and JNI calls

Build:
- Auto-detect Android NDK location
- Clear conflicting environment variables for cross-compilation
- Add androidx.documentfile dependency for DocumentFile API

AndroidManifest:
- Update storage permissions for Android 13+ (granular media permissions)
- Add MANAGE_EXTERNAL_STORAGE for file manager functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…r UI

Transport:
- Add SpacedriveError class with code, errorType, and details
- Add request timeouts (30s default, 2min for long-running operations)
- Detect long-running methods (locations.add, libraries.create, jobs.run)
- Clean up pending timeouts on destroy()

Client hooks:
- Add useMobileClient() helper for properly typed mobile client access
- Fix type casting between ts-client and mobile client interfaces

OverviewScreen:
- Implement handleAddStorage() for adding storage locations
- Android: Use native SAF folder picker via SDMobileCore.pickFolder()
- iOS: Use expo-document-picker
- Show success/error alerts with appropriate messaging
- Handle permission and access errors gracefully

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…s, and volumes

- Add Stack.Screen routes for location, device, and volume detail pages
- Implement navigation from Browse screen to detail screens
- Add LocationExplorerScreen, DeviceDetailsScreen, and VolumeDetailsScreen
- Fix SpaceSwitcher to support space selection state
- Improve SettingsGroup children typing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Split tab layout into iOS (NativeTabs) and Android (Tabs) components
- iOS uses liquid glass effect with SF Symbols
- Android follows Material Design 3 guidelines:
  - 80dp tab bar height with proper safe area handling
  - 24dp icons with filled/outline states
  - Active indicator pill (64x32dp) with animated transitions
  - Brand-aligned color scheme
- Smooth animations using react-native-reanimated:
  - Pill fade in/out
  - Icon/label spacing adjustments on focus
  - 200ms cubic easing for M3 feel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These dedicated screens were superseded by the unified /explorer approach.

Removed:
- Route files: device/[deviceId], location/[locationId], volume/[volumeId]
- Screen components: DeviceDetailsScreen, LocationExplorerScreen, VolumeDetailsScreen
- Stack.Screen definitions in _layout.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace CSS transition-all with react-native-reanimated for space indicator dots
- Add slide_from_right animation for explorer screen navigation
- Use consistent 200ms timing across all animations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Kotlin static analysis tooling:
- detekt v1.23.7 for code quality and complexity checks
- ktlint v12.1.2 for code formatting enforcement

Includes detekt.yml configuration with rules for:
- Complexity thresholds (method length, nesting depth)
- Naming conventions
- Potential bugs detection
- Style consistency

Run with: ./gradlew detekt ktlintCheck

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add two new CI jobs for mobile quality gates:

1. android-lint: Runs detekt and ktlint on Kotlin code
   - Triggers on changes to android/ directories
   - Uses Java 17 + Bun setup

2. mobile-rust-tests: Runs cargo test on sd-mobile-core
   - Triggers on changes to mobile core or shared Rust code
   - Tests FFI layer including safe_cstring and error mapping

Both jobs use path filtering to skip when unrelated files change.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Security hardening for Android app data:

- Set allowBackup="false" to prevent cloud backup of sensitive data
- Add backup_rules.xml excluding databases, prefs, and library data
- Add data_extraction_rules.xml for Android 12+ D2D transfer rules

This prevents Spacedrive library databases and configuration from
being backed up to Google Drive or transferred during device setup,
which could expose user file metadata.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Build configuration improvements:

Release Signing:
- Add signingConfigs.release using environment variables
- Required vars: SPACEDRIVE_KEYSTORE_PATH, _PASSWORD, KEY_ALIAS, KEY_PASSWORD
- Falls back to debug keystore if not configured
- Add SIGNING.md documentation with setup instructions

Dynamic Versioning:
- versionCode now uses CI build number (GITHUB_RUN_NUMBER)
- Falls back to git commit count for local builds
- versionName configurable via SPACEDRIVE_VERSION env var

This enables proper Play Store releases while allowing development
builds without production credentials.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comprehensive safety improvements for the Rust FFI layer:

Null Pointer Validation:
- Add null checks before CStr::from_ptr in initialize_core, handle_core_msg
- Return proper error codes/responses instead of undefined behavior

Panic Prevention:
- Replace .unwrap() on CORE.get() and RUNTIME.get() with match + error
- Replace serde_json::to_value().unwrap() with unwrap_or_else
- Replace .as_object().unwrap() with safe Option::map pattern

Transmute Safety:
- Validate callback function pointers are non-zero before transmute
- Add null check before Box::from_raw in android_callback

Conditional Logging:
- Add debug_log!, info_log!, error_log! macros
- debug_log compiles to nothing in release builds
- Prevents debug output and sensitive data leakage in production

Async Timeouts:
- Add tokio::time::timeout wrapper to process_daemon_request
- 30s default, 120s for long-running methods (locations.add, etc.)
- Returns proper JSON-RPC timeout error instead of hanging

Unit Tests:
- test_safe_cstring_* for null byte handling
- test_daemon_error_* for error code mapping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Thread Safety:
- Replace var listeners with AtomicInteger for concurrent access
- Replace var registeredWithRust with AtomicBoolean
- Replace single pendingFolderPickerPromise with ConcurrentHashMap
- Use unique request codes per folder picker call

Exception Handling:
- Add specific catch blocks (SecurityException, IllegalArgumentException)
- Use compareAndSet for atomic flag updates with rollback on failure

Permission Checks:
- Add hasStoragePermission() for Android 11+ MANAGE_EXTERNAL_STORAGE
- Add warnIfNoStoragePermission() to log when permissions missing

Path Security:
- Add validateAndResolvePath() with canonical path verification
- Check each path component for traversal (.. and .)
- Verify resolved path stays under expected base directory
- Catches symlink-based escape attempts

Debug Logging:
- Add debugLog() that respects app debuggable flag
- Add sanitizePath() that redacts paths in release builds
- Initialize debug state from ApplicationInfo flags

Deprecation Documentation:
- Add @Suppress("DEPRECATION") for startActivityForResult
- Document that Expo modules don't yet support ActivityResultContracts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
transport.ts - Retry Logic:
- Add RetryConfig interface with maxRetries, baseDelayMs, backoffMultiplier
- Add exponential backoff with jitter (10-20% randomization)
- NON_RETRYABLE_ERROR_TYPES list for client errors that shouldn't retry
- Split request() into public method with retry and private requestInternal()

transport.ts - Health Checks:
- Add startHealthCheck() with configurable ping interval
- Add onHealthChange() listener for status updates
- Add performHealthCheck() for manual checks
- Track healthy/unhealthy/unknown status

transport.ts - Batch Timeout:
- Add batch-level timeout (base + per-request)
- Reset batchQueued in finally block for recovery
- Reject all batch requests with specific error types on failure

subscriptionManager.ts - Cleanup Races:
- Add isDestroying flag to prevent cleanup during destruction
- Add cleaned flag to SubscriptionEntry to prevent double cleanup
- Guard createCleanup with hasRun flag
- Defer unsubscribe to next tick with setTimeout
- Clear pendingSubscriptions in destroy()

SpacedriveClient.ts - Health Integration:
- Add startHealthMonitoring() and stopHealthMonitoring()
- Add getHealthStatus() and checkHealth()
- Add onHealthChange() that also emits 'connection-health' event
- Stop health monitoring in destroy()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rust FFI: Add safety documentation and allow attributes for
  pointer dereference functions, remove unused mut
- Kotlin: Refactor complex SAF path resolution functions into
  smaller, focused helpers (parseDocumentId, isRelativePathSafe,
  resolveViaFallbackMounts, resolveVolumeToPath, etc.)
- Kotlin: Add justified suppressions for Expo module API constraints
- Detekt: Disable NoTabs rule (project uses tabs for indentation)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Run cargo fmt on files modified in this branch to fix CI formatting check.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive handling for Android's MANAGE_EXTERNAL_STORAGE permission:

- Add native module functions to check/request storage permission
- Add useStoragePermission hook for React Native
- Add StoragePermissionBanner component on Overview screen
- Add Permissions section in Settings screen
- Update Rust LocalBackend to log permission errors (sanitized paths)

Without this permission on Android 11+, files are silently skipped during
directory listing. Now users see a clear prompt to grant access.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Android-specific layout with parallax hero that scrolls with content
- Hero moves at 0.3x scroll speed with fade effect for depth illusion
- Fixed library name header stays pinned at top
- MY NETWORK header fades in when scrolled past hero section
- Content card overlaps hero with negative margin for slide-over effect
- Fix PageIndicator to use reanimated animations instead of CSS transitions
- Disable transformOrigin scale animations on Android (not well supported)
- Add nestedScrollEnabled to HeroStats for proper horizontal scroll handling
- Keep iOS layout unchanged with its complex z-index parallax system

Platform-specific layouts needed due to different touch event handling:
iOS allows touches through z-index siblings, Android captures all scroll
gestures in topmost ScrollView bounds regardless of pointerEvents.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@StarbirdTech StarbirdTech force-pushed the feat/android-mobile-support branch from 672263f to d3424d3 Compare February 6, 2026 08:44
@StarbirdTech StarbirdTech marked this pull request as ready for review February 6, 2026 09:11
@jamiepine
Copy link
Member

@tembo review again please

@tembo
Copy link
Contributor

tembo bot commented Feb 6, 2026

@jamiepine I'm working on your request now and will update you when I'm done.

View on Tembo

StarbirdTech and others added 2 commits February 6, 2026 04:44
- Rename release-mobile profile to mobile-dev in build script to match Cargo.toml
- Add missing log and android_logger crates for Android build
- Fix useNormalizedQuery calls using wrong wireMethod property instead of query
- Remove network tab route and screen that kept reappearing from rebases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove hard-coded form_factor for iOS/Android (was misclassifying iPhones as Tablet)
- Remove redundant device_exists check that didn't close TOCTOU race
- Canonicalize path in execute() to match validation, preventing path mismatch
- Improve skipped entries warning to be platform-neutral
- Gate Android logger level on debug_assertions to avoid verbose logging in release

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@StarbirdTech
Copy link
Contributor Author

Mobile Feature Config Test Results

This comment was written by Claude Code (Opus 4.6) after running the core test suite under the mobile feature configuration to validate that the Android changes don't break existing core functionality.

Goal

Verify that sd-core compiles and passes its test suite under the same feature configuration used by the mobile build (--no-default-features --features mobile), which disables WASM and enables the mobile feature flag.

Setup

To get the test suite compiling, I applied the Box<dyn Error>Box<dyn Error + Send + Sync> fixes from #3016 locally on this branch. Those are pre-existing compilation failures on main (not caused by this PR).

Results

sd-mobile-core FFI unit tests — 12/12 passed ✓

cargo test --manifest-path apps/mobile/modules/sd-mobile-core/core/Cargo.toml

test result: ok. 12 passed; 0 failed; 0 ignored

Tests cover: C string null-byte safety, unicode handling, DaemonError → JSON-RPC error code mapping.

sd-core library unit tests (--no-default-features --features mobile) — 314 passed, 3 failed

cargo test -p sd-core --no-default-features --features mobile --lib

test result: FAILED. 314 passed; 3 failed; 1 ignored

The 3 failures are pre-existing floating-point precision bugs in ops::indexing::progress::tests (e.g. 0.90999997 != 0.91). Not related to mobile or this PR.

Integration tests that compiled and ran:

Test Result
copy_action_test 7/7 passed ✓
ephemeral_watcher_test 1/1 passed ✓
file_move_test 5/6 passed (1 flaky: test_ephemeral_file_move_via_reindex)

Tests that failed to compile (pre-existing on main, not from this PR):

Test Error
relay_pairing_test ThreadRng: CryptoRng trait bound not satisfied (rand version conflict)
volume_fingerprint_stability_test VolumeFingerprint::new not found (API was renamed)
pause_resume_demo (example) Box<dyn Error> signature mismatch (fixed in #3016)

Conclusion

The mobile feature flag and default-features = false configuration does not introduce any new test failures. All 314 core unit tests + 13 integration tests pass. The failures encountered are all pre-existing on main.

@StarbirdTech
Copy link
Contributor Author

@jamiepine I tested this branch on both the iOS simulator and my personal phone (Pixel 8a, Android 16). I don't believe I've introduced any changes that cause a regression for iOS. I'm comfortable saying this pr is ready to merge.

I'd like to get proper test coverage of the mobile app before claiming it fully "works on Android," but that can be a follow-up PR. Same for polishing the UI to bring it up to par with the iOS version. Also not sure on the state of the Android CI added in this pr, but I don't think i can run that myself (?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants