Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e50923c
chore: update dependencies and module exports
StarbirdTech Jan 20, 2026
8dc40fd
types: add TypeScript declarations for assets and expo-router
StarbirdTech Jan 20, 2026
f34bf5d
feat(core): add Android/iOS platform support for device and volume de…
StarbirdTech Jan 20, 2026
1d78f05
fix(core): improve library ID access and location add safety
StarbirdTech Jan 20, 2026
a93ed3a
feat(android): enhance native module with folder picker and improved …
StarbirdTech Jan 20, 2026
3c00d84
feat(mobile): add request timeouts, error handling, and storage picke…
StarbirdTech Jan 20, 2026
ad22ae8
WIP: feat(mobile): add detail screen navigation for devices, location…
StarbirdTech Jan 20, 2026
06cd694
feat(android): add platform-specific tab navigation with M3 styling
StarbirdTech Jan 21, 2026
2489a39
chore(mobile): remove orphaned detail screen routes and components
StarbirdTech Jan 21, 2026
46c2a66
fix(mobile): add animations for browse tab and explorer navigation
StarbirdTech Jan 21, 2026
790c2a8
chore(android): add detekt and ktlint static analysis
StarbirdTech Jan 21, 2026
18c11d0
ci(android): add lint and test jobs for mobile
StarbirdTech Jan 21, 2026
26dadb7
fix(android): disable allowBackup and secure manifest
StarbirdTech Jan 21, 2026
22dbfd5
build(android): add release signing and dynamic versionCode
StarbirdTech Jan 21, 2026
c30a546
fix(rust): add FFI safety improvements and unit tests
StarbirdTech Jan 21, 2026
d1fb091
fix(android): improve thread safety and security in native module
StarbirdTech Jan 21, 2026
00f2be8
fix(mobile): add retry logic, health checks, and cleanup fixes
StarbirdTech Jan 21, 2026
33048fe
fix(android): pass detekt and clippy linting checks
StarbirdTech Jan 21, 2026
30e0f83
style: fix rust formatting in branch-modified files
StarbirdTech Jan 21, 2026
9a3c564
feat(android): add storage permission check and user prompts
StarbirdTech Jan 21, 2026
d3424d3
feat(mobile): add M3-style collapsing header for Android overview
StarbirdTech Jan 29, 2026
56c9b72
fix(android): fix build errors and remove stale network tab
StarbirdTech Feb 6, 2026
d101a43
fix: apply PR code review feedback from tembo
StarbirdTech Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,95 @@ jobs:
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: bun run typecheck
working-directory: apps/tauri

android-lint:
name: Android Lint
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Check if files have changed
uses: dorny/paths-filter@v3
continue-on-error: true
id: filter
with:
filters: |
changes:
- 'apps/mobile/android/**'
- 'apps/mobile/modules/**/android/**'
- '.github/workflows/ci.yml'

- name: Setup Java
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Bun
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.4"

- name: Install dependencies
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: bun install

- name: Run detekt
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: ./gradlew :sd-mobile-core:detekt --no-daemon
working-directory: apps/mobile/android

- name: Run ktlint
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: ./gradlew :sd-mobile-core:ktlintCheck --no-daemon
working-directory: apps/mobile/android

mobile-rust-tests:
name: Mobile Rust Tests
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
swap-size-mb: 3072
root-reserve-mb: 6144
remove-dotnet: "true"
remove-codeql: "true"
remove-haskell: "true"
remove-docker-images: "true"

- name: Checkout repository
uses: actions/checkout@v4

- name: Check if files have changed
uses: dorny/paths-filter@v3
continue-on-error: true
id: filter
with:
filters: |
changes:
- 'apps/mobile/modules/sd-mobile-core/core/**'
- 'core/**'
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'

- name: Setup System and Rust
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-system
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Run mobile core tests
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
run: cargo test --manifest-path apps/mobile/modules/sd-mobile-core/core/Cargo.toml --locked
91 changes: 91 additions & 0 deletions apps/mobile/android/SIGNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Android Release Signing Configuration

This document explains how to configure release signing for the Spacedrive Android app.

## Overview

Release builds require a signing key to be installed on user devices. Debug builds use a shared debug keystore, but release builds should use a secure, production keystore.

## Environment Variables

The build system looks for the following environment variables:

| Variable | Description |
|----------|-------------|
| `SPACEDRIVE_KEYSTORE_PATH` | Absolute path to your release keystore file (`.jks` or `.keystore`) |
| `SPACEDRIVE_KEYSTORE_PASSWORD` | Password for the keystore |
| `SPACEDRIVE_KEY_ALIAS` | Alias of the signing key within the keystore |
| `SPACEDRIVE_KEY_PASSWORD` | Password for the signing key (often same as keystore password) |

## Setup Instructions

### 1. Generate a Keystore (if you don't have one)

```bash
keytool -genkey -v -keystore spacedrive-release.keystore \
-alias spacedrive \
-keyalg RSA \
-keysize 2048 \
-validity 10000
```

Follow the prompts to set passwords and enter organization details.

### 2. Store the Keystore Securely

- Keep the keystore file in a secure location (NOT in the repository)
- Back up the keystore - losing it means you cannot update your app
- Consider using a secrets manager for CI/CD

### 3. Set Environment Variables

For local development, add to your shell profile (`.bashrc`, `.zshrc`, etc.):

```bash
export SPACEDRIVE_KEYSTORE_PATH="/path/to/spacedrive-release.keystore"
export SPACEDRIVE_KEYSTORE_PASSWORD="your-keystore-password"
export SPACEDRIVE_KEY_ALIAS="spacedrive"
export SPACEDRIVE_KEY_PASSWORD="your-key-password"
```

### 4. Build Release APK

```bash
cd apps/mobile/android
./gradlew assembleRelease
```

The signed APK will be at: `app/build/outputs/apk/release/app-release.apk`

## CI/CD Configuration

For GitHub Actions, add secrets:
- `ANDROID_KEYSTORE_BASE64` - Base64-encoded keystore file
- `ANDROID_KEYSTORE_PASSWORD`
- `ANDROID_KEY_ALIAS`
- `ANDROID_KEY_PASSWORD`

Example workflow step:
```yaml
- name: Decode keystore
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > release.keystore

- name: Build release APK
env:
SPACEDRIVE_KEYSTORE_PATH: ${{ github.workspace }}/release.keystore
SPACEDRIVE_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
SPACEDRIVE_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
SPACEDRIVE_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: ./gradlew assembleRelease
```

## Fallback Behavior

If the environment variables are not set or the keystore file doesn't exist, the build falls back to the debug keystore. This allows developers to build release variants without production keys for testing purposes.

## Security Notes

- Never commit keystores or passwords to version control
- Use different keystores for development and production
- Rotate keys if they may have been compromised
- The Play Store requires consistent signing for app updates
68 changes: 63 additions & 5 deletions apps/mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,43 @@ def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBu
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'

/**
* Calculate dynamic versionCode based on:
* 1. CI build number (GITHUB_RUN_NUMBER or CI_BUILD_NUMBER env var)
* 2. Git commit count as fallback
* 3. Default value of 1 if neither available
*
* This ensures versionCode always increases for Play Store uploads.
*/
def getVersionCode = {
// Priority 1: Use CI build number if available
def ciBuildNumber = System.getenv("GITHUB_RUN_NUMBER") ?: System.getenv("CI_BUILD_NUMBER")
if (ciBuildNumber != null && ciBuildNumber.isInteger()) {
return ciBuildNumber.toInteger()
}

// Priority 2: Count git commits
try {
def gitCommitCount = 'git rev-list --count HEAD'.execute([], rootDir).text.trim()
if (gitCommitCount.isInteger()) {
return gitCommitCount.toInteger()
}
} catch (Exception e) {
logger.warn("Failed to get git commit count: ${e.message}")
}

// Fallback: return 1
return 1
}

/**
* Get version name from environment or default.
* Use SPACEDRIVE_VERSION env var in CI to set semantic version.
*/
def getVersionName = {
return System.getenv("SPACEDRIVE_VERSION") ?: "1.0.0"
}

android {
ndkVersion rootProject.ext.ndkVersion

Expand All @@ -92,8 +129,8 @@ android {
applicationId 'com.spacedrive.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
versionCode getVersionCode()
versionName getVersionName()

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand All @@ -104,15 +141,36 @@ android {
keyAlias 'androiddebugkey'
keyPassword 'android'
}
// Release signing config - uses environment variables for security
// Required env vars for release builds:
// SPACEDRIVE_KEYSTORE_PATH - Path to the release keystore file
// SPACEDRIVE_KEYSTORE_PASSWORD - Password for the keystore
// SPACEDRIVE_KEY_ALIAS - Alias of the signing key
// SPACEDRIVE_KEY_PASSWORD - Password for the signing key
// See: apps/mobile/android/SIGNING.md for setup instructions
release {
def keystorePath = System.getenv("SPACEDRIVE_KEYSTORE_PATH")
if (keystorePath != null && file(keystorePath).exists()) {
storeFile file(keystorePath)
storePassword System.getenv("SPACEDRIVE_KEYSTORE_PASSWORD") ?: ""
keyAlias System.getenv("SPACEDRIVE_KEY_ALIAS") ?: ""
keyPassword System.getenv("SPACEDRIVE_KEY_PASSWORD") ?: ""
} else {
// Fall back to debug keystore if release signing not configured
// This allows development builds without release keys
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
signingConfig signingConfigs.release
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
shrinkResources enableShrinkResources.toBoolean()
minifyEnabled enableMinifyInReleaseBuilds
Expand Down
19 changes: 15 additions & 4 deletions apps/mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<!-- Legacy storage permissions (Android 12 and below) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>

<!-- Android 13+ granular media permissions -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>

<!-- Android 11+ broad access (requires Play Store justification for file managers) -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:fullBackupContent="@xml/backup_rules" android:dataExtractionRules="@xml/data_extraction_rules" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:requestLegacyExternalStorage="true">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
Expand Down
22 changes: 22 additions & 0 deletions apps/mobile/android/app/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Backup rules for Android 11 (API 30) and below.
Excludes sensitive data from cloud backups.
See: https://developer.android.com/guide/topics/data/autobackup
-->
<full-backup-content>
<!-- Exclude database files containing user data -->
<exclude domain="database" path="." />

<!-- Exclude shared preferences that may contain sensitive config -->
<exclude domain="sharedpref" path="." />

<!-- Exclude Spacedrive data directory -->
<exclude domain="file" path="spacedrive" />

<!-- Exclude any cached files -->
<exclude domain="external" path="." />

<!-- Exclude library database files -->
<exclude domain="file" path="libraries" />
</full-backup-content>
27 changes: 27 additions & 0 deletions apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Data extraction rules for Android 12 (API 31) and above.
Controls both cloud backup and device-to-device transfer.
See: https://developer.android.com/about/versions/12/backup-restore
-->
<data-extraction-rules>
<!-- Rules for cloud backup -->
<cloud-backup>
<!-- Exclude all sensitive data from cloud backups -->
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="file" path="spacedrive" />
<exclude domain="file" path="libraries" />
<exclude domain="external" path="." />
</cloud-backup>

<!-- Rules for device-to-device transfer -->
<device-transfer>
<!-- Also exclude sensitive data from D2D transfers -->
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="file" path="spacedrive" />
<exclude domain="file" path="libraries" />
<exclude domain="external" path="." />
</device-transfer>
</data-extraction-rules>
5 changes: 5 additions & 0 deletions apps/mobile/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ buildscript {
}
}

plugins {
id 'io.gitlab.arturbosch.detekt' version '1.23.7' apply false
id 'org.jlleitschuh.gradle.ktlint' version '12.1.2' apply false
}

allprojects {
repositories {
google()
Expand Down
Loading