Initial commit — SyncFlow Android file sync app

Supports WebDAV, SFTP, SFTPGo, Nextcloud, ownCloud, Google Drive,
Dropbox, and OneDrive. Credentials encrypted with Android Keystore.
Biometric app-lock, conflict resolution, and auto-sync via WorkManager.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 20:21:20 +00:00
commit cff4233de6
95 changed files with 5381 additions and 0 deletions
@@ -0,0 +1,120 @@
package com.syncflow.ui.auth
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Base64
import androidx.browser.customtabs.CustomTabsIntent
import com.syncflow.data.security.CredentialStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import java.security.MessageDigest
import java.security.SecureRandom
const val OAUTH_REDIRECT_ACTION = "com.syncflow.OAUTH_RESULT"
const val OAUTH_EXTRA_TOKEN = "token"
const val OAUTH_EXTRA_EMAIL = "email"
const val OAUTH_EXTRA_PROVIDER = "provider"
private val client = OkHttpClient()
private val random = SecureRandom()
private fun generateVerifier(): String {
val bytes = ByteArray(32)
random.nextBytes(bytes)
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
}
private fun generateChallenge(verifier: String): String {
val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII))
return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
}
fun launchDropboxOAuth(context: Context, credentialStore: CredentialStore, appKey: String) {
val verifier = generateVerifier()
credentialStore.savePkceVerifier("dropbox", verifier)
val challenge = generateChallenge(verifier)
val url = "https://www.dropbox.com/oauth2/authorize" +
"?client_id=$appKey" +
"&response_type=code" +
"&redirect_uri=syncflow%3A%2F%2Foauth%2Fdropbox" +
"&code_challenge=$challenge" +
"&code_challenge_method=S256" +
"&token_access_type=offline"
openCustomTab(context, url)
}
fun launchOneDriveOAuth(context: Context, credentialStore: CredentialStore, clientId: String) {
val verifier = generateVerifier()
credentialStore.savePkceVerifier("onedrive", verifier)
val challenge = generateChallenge(verifier)
val scopes = "Files.ReadWrite+User.Read+offline_access"
val url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" +
"?client_id=$clientId" +
"&response_type=code" +
"&redirect_uri=syncflow%3A%2F%2Foauth%2Fonedrive" +
"&scope=$scopes" +
"&code_challenge=$challenge" +
"&code_challenge_method=S256"
openCustomTab(context, url)
}
private fun openCustomTab(context: Context, url: String) {
try {
CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(url))
} catch (_: Exception) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK })
}
}
suspend fun exchangeDropboxCode(
credentialStore: CredentialStore,
code: String,
appKey: String,
): Pair<String, String>? = withContext(Dispatchers.IO) {
val verifier = credentialStore.getPkceVerifier("dropbox") ?: return@withContext null
credentialStore.removePkceVerifier("dropbox")
val body = FormBody.Builder()
.add("code", code)
.add("grant_type", "authorization_code")
.add("client_id", appKey)
.add("redirect_uri", "syncflow://oauth/dropbox")
.add("code_verifier", verifier)
.build()
val req = Request.Builder().url("https://api.dropboxapi.com/oauth2/token").post(body).build()
val resp = runCatching { client.newCall(req).execute() }.getOrNull() ?: return@withContext null
val text = resp.body?.string() ?: return@withContext null
val json = runCatching { Json.parseToJsonElement(text).jsonObject }.getOrNull() ?: return@withContext null
val token = json["access_token"]?.jsonPrimitive?.content ?: return@withContext null
val email = json["account_id"]?.jsonPrimitive?.content ?: ""
Pair(token, email)
}
suspend fun exchangeOneDriveCode(
credentialStore: CredentialStore,
code: String,
clientId: String,
): Pair<String, String>? = withContext(Dispatchers.IO) {
val verifier = credentialStore.getPkceVerifier("onedrive") ?: return@withContext null
credentialStore.removePkceVerifier("onedrive")
val body = FormBody.Builder()
.add("code", code)
.add("grant_type", "authorization_code")
.add("client_id", clientId)
.add("redirect_uri", "syncflow://oauth/onedrive")
.add("code_verifier", verifier)
.add("scope", "Files.ReadWrite User.Read offline_access")
.build()
val req = Request.Builder().url("https://login.microsoftonline.com/common/oauth2/v2.0/token").post(body).build()
val resp = runCatching { client.newCall(req).execute() }.getOrNull() ?: return@withContext null
val text = resp.body?.string() ?: return@withContext null
val json = runCatching { Json.parseToJsonElement(text).jsonObject }.getOrNull() ?: return@withContext null
val token = json["access_token"]?.jsonPrimitive?.content ?: return@withContext null
Pair(token, "")
}