Skip to content

Commit fc8f4ac

Browse files
authored
Add files via upload
1 parent 176023b commit fc8f4ac

46 files changed

Lines changed: 5425 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

android/app/build.gradle.kts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
plugins {
2+
id("com.android.application")
3+
id("org.jetbrains.kotlin.android")
4+
id("org.jetbrains.kotlin.plugin.compose")
5+
id("com.google.devtools.ksp")
6+
}
7+
8+
android {
9+
namespace = "com.synapse.lantransfer"
10+
compileSdk = 35
11+
12+
defaultConfig {
13+
applicationId = "com.synapse.lantransfer"
14+
minSdk = 26
15+
targetSdk = 34
16+
versionCode = 1
17+
versionName = "1.0.0"
18+
}
19+
20+
buildTypes {
21+
release {
22+
signingConfig = signingConfigs.getByName("debug")
23+
isMinifyEnabled = false
24+
proguardFiles(
25+
getDefaultProguardFile("proguard-android-optimize.txt"),
26+
"proguard-rules.pro"
27+
)
28+
}
29+
}
30+
31+
compileOptions {
32+
sourceCompatibility = JavaVersion.VERSION_17
33+
targetCompatibility = JavaVersion.VERSION_17
34+
}
35+
36+
kotlinOptions {
37+
jvmTarget = "17"
38+
}
39+
40+
buildFeatures {
41+
compose = true
42+
}
43+
44+
ksp {
45+
arg("room.schemaLocation", "$projectDir/schemas")
46+
}
47+
}
48+
49+
dependencies {
50+
val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
51+
implementation(composeBom)
52+
53+
// Core
54+
implementation("androidx.core:core-ktx:1.15.0")
55+
implementation("androidx.appcompat:appcompat:1.7.0")
56+
implementation("androidx.documentfile:documentfile:1.0.1")
57+
implementation("com.google.android.material:material:1.12.0")
58+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
59+
implementation("androidx.activity:activity-compose:1.9.3")
60+
61+
// Compose UI
62+
implementation("androidx.compose.ui:ui")
63+
implementation("androidx.compose.ui:ui-graphics")
64+
implementation("androidx.compose.ui:ui-tooling-preview")
65+
implementation("androidx.compose.material3:material3")
66+
implementation("androidx.compose.material:material-icons-extended")
67+
implementation("androidx.compose.animation:animation")
68+
implementation("androidx.compose.foundation:foundation")
69+
70+
// Navigation
71+
implementation("androidx.navigation:navigation-compose:2.8.5")
72+
73+
// System UI Controller
74+
implementation("com.google.accompanist:accompanist-systemuicontroller:0.36.0")
75+
76+
// Coil for image loading
77+
implementation("io.coil-kt:coil-compose:2.7.0")
78+
79+
// Room Database
80+
implementation("androidx.room:room-runtime:2.6.1")
81+
implementation("androidx.room:room-ktx:2.6.1")
82+
ksp("androidx.room:room-compiler:2.6.1")
83+
84+
// DataStore Preferences
85+
implementation("androidx.datastore:datastore-preferences:1.1.1")
86+
87+
// Lifecycle & ViewModel
88+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
89+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
90+
91+
// Coroutines
92+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
93+
94+
// Debug
95+
debugImplementation("androidx.compose.ui:ui-tooling")
96+
debugImplementation("androidx.compose.ui:ui-test-manifest")
97+
}

android/app/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Proguard Rules for Synapse
2+
# Add project specific ProGuard rules here.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 1,
5+
"identityHash": "0660dfc99c8ce53a9bd7ba9179af15bd",
6+
"entities": [
7+
{
8+
"tableName": "transfers",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fileName` TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `direction` TEXT NOT NULL, `status` TEXT NOT NULL, `peerName` TEXT NOT NULL, `peerAddress` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `errorMessage` TEXT)",
10+
"fields": [
11+
{
12+
"fieldPath": "id",
13+
"columnName": "id",
14+
"affinity": "INTEGER",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "fileName",
19+
"columnName": "fileName",
20+
"affinity": "TEXT",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "fileSize",
25+
"columnName": "fileSize",
26+
"affinity": "INTEGER",
27+
"notNull": true
28+
},
29+
{
30+
"fieldPath": "direction",
31+
"columnName": "direction",
32+
"affinity": "TEXT",
33+
"notNull": true
34+
},
35+
{
36+
"fieldPath": "status",
37+
"columnName": "status",
38+
"affinity": "TEXT",
39+
"notNull": true
40+
},
41+
{
42+
"fieldPath": "peerName",
43+
"columnName": "peerName",
44+
"affinity": "TEXT",
45+
"notNull": true
46+
},
47+
{
48+
"fieldPath": "peerAddress",
49+
"columnName": "peerAddress",
50+
"affinity": "TEXT",
51+
"notNull": true
52+
},
53+
{
54+
"fieldPath": "timestamp",
55+
"columnName": "timestamp",
56+
"affinity": "INTEGER",
57+
"notNull": true
58+
},
59+
{
60+
"fieldPath": "errorMessage",
61+
"columnName": "errorMessage",
62+
"affinity": "TEXT",
63+
"notNull": false
64+
}
65+
],
66+
"primaryKey": {
67+
"autoGenerate": true,
68+
"columnNames": [
69+
"id"
70+
]
71+
},
72+
"indices": [],
73+
"foreignKeys": []
74+
}
75+
],
76+
"views": [],
77+
"setupQueries": [
78+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
79+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0660dfc99c8ce53a9bd7ba9179af15bd')"
80+
]
81+
}
82+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
7+
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
8+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
9+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
10+
11+
<!-- Granular media permissions for Android 13+ (API 33) -->
12+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
13+
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
14+
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
15+
16+
<!-- Foreground service for active transfers -->
17+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
18+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
19+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
20+
21+
<application
22+
android:allowBackup="true"
23+
android:icon="@drawable/pigeon_logo"
24+
android:label="@string/app_name"
25+
android:roundIcon="@drawable/pigeon_logo"
26+
android:supportsRtl="true"
27+
android:theme="@style/Theme.Synapse"
28+
android:usesCleartextTraffic="false">
29+
<activity
30+
android:name=".MainActivity"
31+
android:exported="true"
32+
android:theme="@style/Theme.Synapse">
33+
<intent-filter>
34+
<action android:name="android.intent.action.MAIN" />
35+
<category android:name="android.intent.category.LAUNCHER" />
36+
</intent-filter>
37+
</activity>
38+
39+
<service
40+
android:name=".data.service.TransferForegroundService"
41+
android:foregroundServiceType="dataSync"
42+
android:exported="false" />
43+
</application>
44+
45+
</manifest>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.synapse.lantransfer
2+
3+
import android.Manifest
4+
import android.content.pm.PackageManager
5+
import android.os.Build
6+
import android.os.Bundle
7+
import androidx.activity.ComponentActivity
8+
import androidx.activity.compose.setContent
9+
import androidx.activity.result.contract.ActivityResultContracts
10+
import androidx.core.content.ContextCompat
11+
import androidx.core.view.WindowCompat
12+
import com.synapse.lantransfer.ui.navigation.SynapseApp
13+
import com.synapse.lantransfer.ui.theme.SynapseTheme
14+
15+
class MainActivity : ComponentActivity() {
16+
17+
private val permissionLauncher = registerForActivityResult(
18+
ActivityResultContracts.RequestMultiplePermissions()
19+
) { /* Permissions handled — the app works with or without them */ }
20+
21+
override fun onCreate(savedInstanceState: Bundle?) {
22+
super.onCreate(savedInstanceState)
23+
24+
// Let Compose handle the system windows (draw under status bar/nav bar)
25+
WindowCompat.setDecorFitsSystemWindows(window, false)
26+
27+
// Request necessary permissions
28+
requestPermissions()
29+
30+
setContent {
31+
SynapseTheme {
32+
SynapseApp()
33+
}
34+
}
35+
}
36+
37+
private fun requestPermissions() {
38+
val permissions = mutableListOf<String>()
39+
40+
// Network permissions (always needed)
41+
if (!hasPermission(Manifest.permission.INTERNET)) {
42+
permissions.add(Manifest.permission.INTERNET)
43+
}
44+
45+
// Storage permissions
46+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
47+
// Android 13+ uses granular media permissions
48+
if (!hasPermission(Manifest.permission.READ_MEDIA_IMAGES))
49+
permissions.add(Manifest.permission.READ_MEDIA_IMAGES)
50+
if (!hasPermission(Manifest.permission.READ_MEDIA_VIDEO))
51+
permissions.add(Manifest.permission.READ_MEDIA_VIDEO)
52+
if (!hasPermission(Manifest.permission.READ_MEDIA_AUDIO))
53+
permissions.add(Manifest.permission.READ_MEDIA_AUDIO)
54+
// Notification permission for foreground service
55+
if (!hasPermission(Manifest.permission.POST_NOTIFICATIONS))
56+
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
57+
} else {
58+
// Legacy storage permissions
59+
if (!hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE))
60+
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE)
61+
if (!hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))
62+
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
63+
}
64+
65+
if (permissions.isNotEmpty()) {
66+
permissionLauncher.launch(permissions.toTypedArray())
67+
}
68+
}
69+
70+
private fun hasPermission(permission: String): Boolean {
71+
return ContextCompat.checkSelfPermission(this, permission) ==
72+
PackageManager.PERMISSION_GRANTED
73+
}
74+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.synapse.lantransfer.data.local
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import android.os.Environment
6+
import androidx.datastore.core.DataStore
7+
import androidx.datastore.preferences.core.*
8+
import androidx.datastore.preferences.preferencesDataStore
9+
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.coroutines.flow.map
11+
import java.io.File
12+
13+
/** DataStore instance scoped to the application context. */
14+
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "synapse_settings")
15+
16+
/**
17+
* Manages user preferences using Jetpack DataStore.
18+
* Provides reactive flows for all settings and suspend functions for mutations.
19+
*/
20+
class PreferencesManager(private val context: Context) {
21+
22+
companion object {
23+
private val KEY_DEVICE_NAME = stringPreferencesKey("device_name")
24+
private val KEY_DOWNLOAD_DIR = stringPreferencesKey("download_dir")
25+
private val KEY_AUTO_ACCEPT = booleanPreferencesKey("auto_accept")
26+
27+
/** Default download directory */
28+
fun defaultDownloadDir(): String {
29+
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
30+
return File(downloads, "Synapse").absolutePath
31+
}
32+
33+
/** Default device name based on the device model */
34+
fun defaultDeviceName(): String {
35+
val manufacturer = Build.MANUFACTURER.replaceFirstChar { it.uppercase() }
36+
val model = Build.MODEL
37+
return if (model.startsWith(manufacturer, ignoreCase = true)) {
38+
model
39+
} else {
40+
"$manufacturer $model"
41+
}
42+
}
43+
}
44+
45+
/** Reactive flow of the device name setting. */
46+
val deviceName: Flow<String> = context.dataStore.data.map { prefs ->
47+
prefs[KEY_DEVICE_NAME] ?: defaultDeviceName()
48+
}
49+
50+
/** Reactive flow of the download directory setting. */
51+
val downloadDir: Flow<String> = context.dataStore.data.map { prefs ->
52+
prefs[KEY_DOWNLOAD_DIR] ?: defaultDownloadDir()
53+
}
54+
55+
/** Reactive flow of the auto-accept setting. */
56+
val autoAccept: Flow<Boolean> = context.dataStore.data.map { prefs ->
57+
prefs[KEY_AUTO_ACCEPT] ?: false
58+
}
59+
60+
suspend fun setDeviceName(name: String) {
61+
context.dataStore.edit { prefs ->
62+
prefs[KEY_DEVICE_NAME] = name
63+
}
64+
}
65+
66+
suspend fun setDownloadDir(dir: String) {
67+
context.dataStore.edit { prefs ->
68+
prefs[KEY_DOWNLOAD_DIR] = dir
69+
}
70+
}
71+
72+
suspend fun setAutoAccept(enabled: Boolean) {
73+
context.dataStore.edit { prefs ->
74+
prefs[KEY_AUTO_ACCEPT] = enabled
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)