A modern Android application for bidirectional synchronization between local device folders and Google Drive. Built with Kotlin, Jetpack Compose, and Clean Architecture principles.
Perfect for syncing your Obsidian vault between devices using Google Drive!
Obsidian stores notes as local Markdown files, but lacks built-in cloud sync on Android. FolderSync bridges this gap:
📱 Phone (Obsidian Vault) ☁️ Google Drive 💻 Desktop (Obsidian Vault)
/Obsidian/MyVault ←→ /Obsidian/MyVault ←→ ~/Documents/Obsidian/MyVault
- On your phone: Point FolderSync to your local Obsidian vault folder
- On Google Drive: Select or create a folder for your vault
- Enable background sync: Your notes sync automatically every 15 minutes
- On desktop: Use Google Drive desktop app or Obsidian Git plugin to sync
- ✅ Free - No Obsidian Sync subscription needed ($96/year saved!)
- ✅ Works offline - Edit notes without internet, sync when connected
- ✅ Conflict detection - Never lose edits when the same note is changed on multiple devices
- ✅ Markdown files stay local - Full control over your data
- ✅ Background sync - Set it and forget it
- Create your Obsidian vault in a folder like
/storage/emulated/0/Obsidian/MyVault - In FolderSync, add a sync pair:
- Local Folder: Your Obsidian vault folder
- Drive Folder: Create
/Obsidian/MyVaulton Drive
- Enable Background Sync with 15-minute interval
- On desktop, sync the same Drive folder to your computer
- Features
- Architecture
- Technical Stack
- Prerequisites
- Build Instructions
- Installation
- Usage Guide
- Sync Logic
- Database Schema
- Troubleshooting
- Contributing
- License
- 🔄 Bidirectional Sync: Sync files both ways between local folders and Google Drive
- 📁 Subfolder Support: Full recursive sync of nested folder structures
- 🔐 Secure Authentication: OAuth 2.0 with Google Sign-In and automatic token refresh
- ⚡ Resumable Uploads/Downloads: Large file transfers can resume after interruption
- 🔍 Smart Conflict Detection: Detects and handles conflicting changes on both sides
- 📄 Google Docs Export: Automatically exports Google Docs/Sheets/Slides to Office formats
- 🔄 Background Sync: Configurable periodic sync using WorkManager (1 min - 24 hours)
- 💾 Database-Tracked State: Accurate create/update/delete detection using Room database
- 🎨 Material 3 UI: Modern, clean interface with Jetpack Compose
- 🚦 Rate Limiting: Built-in exponential backoff for API rate limits
- 📱 Auto-Resume on App Start: Sync schedule automatically restores when app launches
- 🔋 Battery-Friendly: Smart constraints for background sync (WiFi-only, charging-only options)
- 🔄 Update vs Create: Intelligently updates existing files instead of creating duplicates
FolderSync follows Clean Architecture principles with clear separation of concerns across three layers.
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Screens │ │ ViewModels │ │ Navigation │ │
│ │ (Compose) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ DOMAIN LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Models │ │ Use Cases │ │ Sync Engine │ │
│ │ │ │ │ │ (SyncDiffer) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ DATA LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Room │ │ Retrofit │ │ Repositories │ │
│ │ Database │ │ (Drive API) │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
com.foldersync/
├── FolderSyncApp.kt # Application class (Hilt entry point)
├── auth/
│ └── GoogleAuthManager.kt # Google Sign-In orchestration
├── core/
│ └── ... # Core utilities
├── data/
│ ├── auth/
│ │ └── TokenRefreshManager.kt # OAuth token refresh with caching
│ ├── local/
│ │ ├── db/
│ │ │ ├── AppDatabase.kt # Room database
│ │ │ ├── SyncFileDao.kt # DAO for sync tracking
│ │ │ └── SyncLogDao.kt # DAO for sync logs
│ │ ├── entity/
│ │ │ ├── SyncFileEntity.kt # Tracked file state
│ │ │ └── SyncLogEntity.kt # Sync operation logs
│ │ ├── FileSystemManager.kt # SAF file operations
│ │ ├── ChecksumCalculator.kt # MD5 checksum for files
│ │ └── PreferencesManager.kt # DataStore preferences
│ ├── remote/
│ │ └── drive/
│ │ ├── DriveApiService.kt # Retrofit API interface
│ │ ├── DriveFileManager.kt # Drive operations facade
│ │ ├── AuthInterceptor.kt # OAuth header injection
│ │ ├── RateLimiter.kt # Exponential backoff
│ │ ├── model/ # API DTOs
│ │ ├── upload/ # Resumable upload logic
│ │ └── error/ # API exceptions
│ └── repository/
│ └── SyncRepository.kt # Data layer facade
├── di/
│ ├── AppModule.kt # Core dependencies
│ ├── AuthModule.kt # Auth dependencies
│ ├── DatabaseModule.kt # Room database
│ ├── NetworkModule.kt # Retrofit/OkHttp
│ ├── RepositoryModule.kt # Repositories
│ └── WorkerModule.kt # WorkManager
├── domain/
│ ├── model/
│ │ ├── DriveFile.kt # Domain model
│ │ ├── SyncPair.kt # Folder pair config
│ │ └── SyncProgress.kt # Sync state model
│ ├── sync/
│ │ ├── SyncDiffer.kt # Diff algorithm
│ │ └── SyncEngineV2.kt # Sync orchestrator
│ └── usecase/
│ └── ... # Business logic use cases
├── sync/
│ └── ... # Legacy sync components
├── ui/
│ ├── MainActivity.kt # Single activity
│ ├── components/ # Reusable Compose components
│ ├── navigation/ # Navigation graph
│ ├── screens/
│ │ ├── HomeScreen.kt # Main dashboard
│ │ ├── HomeViewModel.kt # Home state management
│ │ ├── SettingsScreen.kt # App settings
│ │ ├── SettingsViewModel.kt # Settings state
│ │ ├── FolderSelectScreen.kt # Local folder picker
│ │ ├── DriveFolderSelectScreen.kt # Drive folder picker
│ │ └── ConflictResolutionScreen.kt # Conflict UI
│ └── theme/ # Material 3 theming
├── util/
│ └── ... # Utility extensions
└── worker/
└── SyncWorker.kt # Background sync worker
The brain of the sync logic. Compares local files, Drive files, and database state to produce a diff:
data class SyncDiff(
val localOnlyFiles: List<LocalFile>, // New local files → upload
val driveOnlyFiles: List<DriveFile>, // New Drive files → download
val modifiedFiles: List<ModifiedFile>, // Changed files → sync
val deletedLocal: List<SyncFileEntity>, // Deleted locally → delete from Drive
val deletedDrive: List<SyncFileEntity>, // Deleted on Drive → delete local
val conflicts: List<ConflictPair>, // Both modified → user decision
val pendingUploads: List<SyncFileEntity>, // Failed uploads to retry
val pendingDownloads: List<SyncFileEntity> // Failed downloads to retry
)Orchestrates the sync process:
- Fetches local and Drive file lists
- Uses
SyncDifferto compute changes - Applies changes (upload/download/delete)
- Updates database state
- Handles errors with retry states
Facade for all Google Drive operations:
- File listing with pagination
- Resumable uploads with progress
- Downloads with range support
- Google Docs export to Office formats
- Folder creation and file deletion
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ UI Layer │───▶│ ViewModel │───▶│ SyncEngine │
│ (Compose) │ │ │ │ │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│FileSystemMgr │ │SyncRepository│ │DriveFileMgr │
│ (Local SAF) │ │ (Database) │ │ (Drive API) │
└──────────────┘ └──────────────┘ └──────────────┘
Using Hilt for dependency injection with the following modules:
| Module | Provides |
|---|---|
AppModule |
Application context, dispatchers, DataStore |
AuthModule |
GoogleSignInClient, TokenRefreshManager |
DatabaseModule |
Room database, DAOs |
NetworkModule |
OkHttpClient, Retrofit, DriveApiService |
RepositoryModule |
SyncRepository |
WorkerModule |
WorkManager configuration |
| Category | Technology |
|---|---|
| Language | Kotlin 1.9 |
| Min SDK | Android 8.0 (API 26) |
| Target SDK | Android 14 (API 34) |
| UI Framework | Jetpack Compose with Material 3 |
| Architecture | MVVM + Clean Architecture |
| DI | Hilt (Dagger) |
| Database | Room |
| Networking | Retrofit + OkHttp |
| Background | WorkManager |
| Auth | Google Sign-In (Play Services) |
| Storage | Storage Access Framework (SAF) |
| Preferences | DataStore |
| Async | Kotlin Coroutines + Flow |
-
JDK 17 - Required for building
# macOS (Homebrew) brew install openjdk@17 # Verify /opt/homebrew/opt/openjdk@17/bin/java -version
-
Android SDK - API 34 (Android 14)
-
Google Cloud Console Project with:
- Google Drive API enabled
- OAuth 2.0 credentials configured
-
Android Device or Emulator - API 26+ with Google Play Services
git clone https://github.com/sureshsankaran/foldersync.git
cd foldersync-
Go to Google Cloud Console
-
Create a new project or select existing one
-
Enable the Google Drive API:
- Navigate to "APIs & Services" → "Library"
- Search for "Google Drive API"
- Click "Enable"
-
Configure OAuth Consent Screen:
- Navigate to "APIs & Services" → "OAuth consent screen"
- Choose "External" user type
- Fill in required fields (App name, User support email, Developer email)
- Add scope:
https://www.googleapis.com/auth/drive - Add test users if in testing mode
-
Create OAuth 2.0 Credentials:
- Navigate to "APIs & Services" → "Credentials"
- Click "Create Credentials" → "OAuth client ID"
Create TWO credentials:
a) Android Client (for app signing):
- Application type: Android
- Package name:
com.foldersync - SHA-1 fingerprint: Get from your debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
b) Web Client (for token exchange):
- Application type: Web application
- Name: "FolderSync Web Client"
- No redirect URIs needed
- Copy the Client ID - you'll need this!
Create/edit local.properties in the project root:
# SDK location (auto-generated by Android Studio)
sdk.dir=/Users/YOUR_USERNAME/Library/Android/sdk
# Google OAuth Web Client ID (from step 2.5b above)
WEB_CLIENT_ID=YOUR_WEB_CLIENT_ID.apps.googleusercontent.com
⚠️ Security Note:local.propertiesis gitignored. Never commit OAuth credentials.
# Set Java 17 and build
JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew assembleDebug
# Output APK location:
# app/build/outputs/apk/debug/app-debug.apkBuild Variants:
| Command | Description |
|---|---|
./gradlew assembleDebug |
Debug build with debugging enabled |
./gradlew assembleRelease |
Release build (requires signing config) |
./gradlew bundleRelease |
Android App Bundle for Play Store |
# Ensure device is connected and USB debugging enabled
adb devices
# Install the APK
adb install -r app/build/outputs/apk/debug/app-debug.apk# Build and install in one command
JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew installDebugTroubleshooting Installation:
# Uninstall existing version first
adb uninstall com.foldersync
# Install with verbose output
adb install -r -t app/build/outputs/apk/debug/app-debug.apk
# Check for errors
adb logcat | grep -i "install"-
Launch the app - FolderSync icon appears in app drawer
-
Sign in with Google:
- Tap "Sign in with Google" button
- Select your Google account
- Grant Drive access permissions
-
Configure a Sync Pair:
- Tap the "+" button on the home screen
- Select Local Folder: Choose a folder from your device
- Select Drive Folder: Choose or create a folder in Google Drive
- The sync pair appears on the home screen
-
From the Home Screen, locate your sync pair
-
Tap the Sync button (circular arrows icon)
-
Watch progress:
- Progress bar shows overall completion
- Status text shows current operation
- File counts show processed/total
-
Sync completes with success or error notification
Enable automatic periodic sync:
-
Open Settings (gear icon)
-
Toggle "Enable Background Sync"
-
Configure sync interval:
- 1 minute (debug/testing)
- 5 minutes (debug/testing)
- 15 minutes
- 30 minutes
- 1 hour
- 2 hours
- 6 hours
- 12 hours
- 24 hours
-
Optional constraints:
- "WiFi Only" - sync only on WiFi networks
- "Charging Only" - sync only when device is charging
Note: Intervals under 15 minutes use a special debug mode with chained one-time work requests instead of periodic work, as WorkManager has a 15-minute minimum for periodic tasks.
Note: The sync schedule is automatically restored when the app starts, so you don't need to reconfigure after a device restart or app update.
When the same file is modified on both local and Drive:
-
Conflict screen appears during sync
-
For each conflict, choose:
- Keep Local - Upload local version, overwrite Drive
- Keep Remote - Download Drive version, overwrite local
- Keep Both - Rename local file, keep both versions
- Skip - Do nothing, handle later
-
Tap "Apply" to resolve conflicts
| Setting | Description |
|---|---|
| Background Sync | Enable/disable periodic sync |
| Sync Interval | How often to sync (1min - 24hr) |
| WiFi Only | Only sync on WiFi networks |
| Charging Only | Only sync when device is charging |
| Conflict Strategy | Default conflict resolution |
| Sign Out | Disconnect Google account |
| Clear Sync History | Reset sync tracking database |
-
Android 12+ (API 31+): Background foreground service restrictions may cause sync to run without a notification when app is in background. Sync still works but won't show progress notification.
-
Samsung Devices: Aggressive battery optimization (FreecessController) may delay background syncs. For best results:
- Add FolderSync to battery optimization whitelist
- Go to Settings → Apps → FolderSync → Battery → Unrestricted
-
WorkManager Minimum Interval: Android's WorkManager has a 15-minute minimum for periodic tasks. Shorter intervals (1-5 min) use a workaround with chained one-time work requests.
Each tracked file can be in one of these states:
| State | Description |
|---|---|
SYNCED |
File is synchronized, no changes |
PENDING_UPLOAD |
Local change needs upload |
PENDING_DOWNLOAD |
Drive change needs download |
ERROR |
Sync failed, will retry |
CONFLICT |
Both sides changed, needs resolution |
The sync engine compares files using:
- Existence Check: Is file present locally and/or on Drive?
- Modification Time: Last modified timestamp comparison
- Checksum (optional): MD5 hash for content verification
- Database State: Previous known state from tracking database
Decision Matrix:
| Local | Drive | Database | Action |
|---|---|---|---|
| New | - | - | Upload |
| - | New | - | Download |
| Modified | Same | Synced | Upload |
| Same | Modified | Synced | Download |
| Modified | Modified | Synced | Conflict |
| Deleted | Exists | Synced | Delete from Drive |
| Exists | Deleted | Synced | Delete local |
Google Docs/Sheets/Slides files cannot be downloaded directly. They are automatically exported:
| Google Type | Export Format | Extension |
|---|---|---|
| Google Docs | Office Word | .docx |
| Google Sheets | Office Excel | .xlsx |
| Google Slides | Office PowerPoint | .pptx |
| Google Drawings | PNG Image | .png |
Tracks the state of each synchronized file:
CREATE TABLE sync_files (
id TEXT PRIMARY KEY, -- Unique file ID
syncPairId TEXT NOT NULL, -- Parent sync pair
localPath TEXT NOT NULL, -- Relative local path
driveId TEXT, -- Google Drive file ID
drivePath TEXT, -- Relative Drive path
localModified INTEGER, -- Local modification timestamp
driveModified INTEGER, -- Drive modification timestamp
localChecksum TEXT, -- MD5 checksum
driveChecksum TEXT, -- Drive MD5 checksum
status TEXT NOT NULL, -- SyncStatus enum
lastSyncTime INTEGER, -- Last successful sync
errorMessage TEXT -- Last error if any
);Logs all sync operations for debugging:
CREATE TABLE sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
action TEXT NOT NULL, -- UPLOAD, DOWNLOAD, DELETE, etc.
filePath TEXT NOT NULL,
fileName TEXT NOT NULL,
success INTEGER NOT NULL,
errorMessage TEXT,
bytesTransferred INTEGER,
durationMs INTEGER
);1. "Sign-in failed" or "Authentication error"
- Verify WEB_CLIENT_ID in local.properties matches Google Cloud Console
- Ensure SHA-1 fingerprint is registered for debug keystore
- Check that Google Drive API is enabled
- Verify test user is added in OAuth consent screen
2. "Session expired" / Frequent re-auth
- The app caches tokens and refreshes automatically
- If persisting, try: Settings → Sign Out → Sign In again
- Check device time is correct (OAuth tokens are time-sensitive)
3. "Download failed: fileNotDownloadable"
- This occurs for Google Docs/Sheets/Slides files
- Fixed in latest version - files are exported to Office formats
- Update to latest version if seeing this error
4. "Rate limit exceeded"
- The app has built-in rate limiting with exponential backoff
- If persisting, wait a few minutes and try again
- Large initial syncs may take time due to API limits
5. Build fails with "Java version" error
# Ensure you're using Java 17
export JAVA_HOME=/opt/homebrew/opt/openjdk@17
./gradlew clean assembleDebug6. PNG files downloaded as JPG
- Fixed in latest version - MIME type is now inferred from file extension
- Update to latest version if seeing this issue
7. Duplicate files created in Google Drive
- Fixed in latest version - existing files are now updated instead of creating new copies
- The sync engine now uses updateFile() for existing files, uploadFile() for new files
8. Background sync not triggering on Samsung
- Samsung's aggressive battery optimization may block background work
- Solution: Add FolderSync to battery optimization whitelist
Settings → Apps → FolderSync → Battery → Unrestricted
- Or use the 1-minute debug interval which uses chained OneTimeWork
9. "ForegroundServiceStartNotAllowedException" in logs
- This is expected on Android 12+ when sync runs from background
- The sync still works - this error is handled gracefully
- Only the progress notification won't show
# All FolderSync logs
adb logcat | grep -E "FolderSync|SyncEngine|DriveFileManager"
# Sync operations only
adb logcat | grep -E "SyncEngineV2|SyncDiffer|SyncWorker"
# Background sync scheduling
adb logcat | grep -E "SyncScheduler|SyncWorker|FolderSyncApp"
# Auth issues
adb logcat | grep -E "TokenRefresh|GoogleAuth|AuthInterceptor"
# Clear and watch live
adb logcat -c && adb logcat | grep -i foldersync- ✅ Bidirectional sync between local folders and Google Drive
- ✅ Subfolder support with recursive sync
- ✅ Google Docs/Sheets/Slides export to Office formats
- ✅ Background sync with configurable intervals (1 min - 24 hours)
- ✅ Auto-restore sync schedule on app start
- ✅ Conflict detection and resolution
- ✅ Database-tracked file state for accurate sync decisions
- ✅ Update existing files (no duplicate creation)
- ✅ Proper MIME type handling for all file types
- ✅ Graceful handling of delete failures (404/403)
- ✅ Samsung battery optimization workarounds
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
- Follow Kotlin coding conventions
- Use meaningful commit messages
- Add KDoc comments for public APIs
- Write unit tests for business logic
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by Suresh Sankaran