diff --git a/.editorconfig b/.editorconfig index 015d3bc..325425c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -32,3 +32,6 @@ trim_trailing_whitespace = false [**/test/**.kt] max_line_length=off + +[**/build/**/*.kt] +ktlint = disabled diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10d3d33..842cdd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,10 @@ jobs: - name: Validate Gradle wrapper uses: gradle/actions/wrapper-validation@v3 + - name: Decode Google Services config + run: | + echo "${{ secrets.GOOGLE_SERVICES_JSON }}" | base64 --decode > composeApp/google-services.json + - name: Build debug APK run: ./gradlew assembleDebug @@ -55,6 +59,10 @@ jobs: echo "${{ secrets.KEYSTORE_ENCRYPTED }}" > keystore.asc gpg -d --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" --batch keystore.asc > keystore.jks + - name: Decode Google Services config + run: | + echo "${{ secrets.GOOGLE_SERVICES_JSON }}" | base64 --decode > composeApp/google-services.json + - name: Build release APK run: ./gradlew assembleRelease @@ -62,7 +70,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ark-drop-release - path: ./app/build/outputs/apk/release/ark-drop-release.apk + path: ./composeApp/build/outputs/apk/release/composeApp-release.apk lint: environment: Development @@ -82,7 +90,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: lint-results - path: ./app/build/reports/*.html + path: ./composeApp/build/reports/*.html ktlint: environment: Development diff --git a/.github/workflows/release-ios.yml b/.github/workflows/release-ios.yml new file mode 100644 index 0000000..e04a1eb --- /dev/null +++ b/.github/workflows/release-ios.yml @@ -0,0 +1,396 @@ +name: Release iOS App + +# Trigger: push tags or manual dispatch +on: + push: + tags: + - 'v*' + branches: + # TEMPORARY: Remove after testing + - 'feature/kmp-ios-impl' + - 'fix/kmp-ios-impl' + - 'fix-and-firebase/kmp-ios-impl' + workflow_dispatch: + +jobs: + build: + runs-on: macos-26 # macOS 26 Tahoe with Xcode 26.2 (required for App Store from Apr 28, 2026 - ITMS-90725) + environment: Testflight + + env: + APP_BUNDLE_ID: dev.ark-builders.drop + APP_TEAM_ID: SQNXHTL7FT + IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + # 1️⃣ Checkout code + - name: Checkout repository + uses: actions/checkout@v4 + + # 🔍 Diagnostic: Check all secrets availability + - name: Validate Secrets + run: | + echo "🔍 Checking which secrets are available..." + echo "" + + # Check each required secret + MISSING_SECRETS=() + + if [ -z "${{ secrets.IOS_P12_BASE64 }}" ]; then + echo "❌ IOS_P12_BASE64 - MISSING or EMPTY" + MISSING_SECRETS+=("IOS_P12_BASE64") + else + echo "✅ IOS_P12_BASE64 - Available (${#IOS_P12_BASE64} chars)" + fi + + if [ -z "${{ secrets.IOS_P12_PASSWORD }}" ]; then + echo "❌ IOS_P12_PASSWORD - MISSING or EMPTY" + MISSING_SECRETS+=("IOS_P12_PASSWORD") + else + echo "✅ IOS_P12_PASSWORD - Available" + fi + + if [ -z "${{ secrets.IOS_PROFILE_BASE64 }}" ]; then + echo "❌ IOS_PROFILE_BASE64 - MISSING or EMPTY" + MISSING_SECRETS+=("IOS_PROFILE_BASE64") + else + echo "✅ IOS_PROFILE_BASE64 - Available (${#IOS_PROFILE_BASE64} chars)" + fi + + if [ -z "${{ secrets.ASC_API_KEY_BASE64 }}" ]; then + echo "❌ ASC_API_KEY_BASE64 - MISSING or EMPTY" + MISSING_SECRETS+=("ASC_API_KEY_BASE64") + else + echo "✅ ASC_API_KEY_BASE64 - Available (${#ASC_API_KEY_BASE64} chars)" + fi + + if [ -z "${{ secrets.ASC_KEY_ID }}" ]; then + echo "❌ ASC_KEY_ID - MISSING or EMPTY" + MISSING_SECRETS+=("ASC_KEY_ID") + else + echo "✅ ASC_KEY_ID - Available" + fi + + if [ -z "${{ secrets.ASC_ISSUER_ID }}" ]; then + echo "❌ ASC_ISSUER_ID - MISSING or EMPTY" + MISSING_SECRETS+=("ASC_ISSUER_ID") + else + echo "✅ ASC_ISSUER_ID - Available" + fi + + if [ -z "${{ secrets.GOOGLE_SERVICE_INFO_BASE64 }}" ]; then + echo "❌ GOOGLE_SERVICE_INFO_BASE64 - MISSING or EMPTY" + MISSING_SECRETS+=("GOOGLE_SERVICE_INFO_BASE64") + else + echo "✅ GOOGLE_SERVICE_INFO_BASE64 - Available" + fi + + echo "" + echo "📊 Summary: ${#MISSING_SECRETS[@]} secrets missing" + + if [ ${#MISSING_SECRETS[@]} -gt 0 ]; then + echo "" + echo "⚠️ Missing secrets need to be added to:" + echo " Settings → Environments → Testflight → Environment secrets" + echo "" + echo "Current repository: ${{ github.repository }}" + echo "Current environment: Testflight" + exit 1 + fi + + echo "" + echo "✅ All required secrets are available!" + env: + IOS_P12_BASE64: ${{ secrets.IOS_P12_BASE64 }} + IOS_PROFILE_BASE64: ${{ secrets.IOS_PROFILE_BASE64 }} + ASC_API_KEY_BASE64: ${{ secrets.ASC_API_KEY_BASE64 }} + + # 2️⃣ Setup Ruby and Fastlane + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + bundler-cache: true + + - name: Install gems + run: bundle install + + # 3️⃣ Build Kotlin framework before Xcode (required for KMP - Xcode expects it at XCFrameworks/debug) + - name: Build Kotlin framework for Release + run: | + export JAVA_HOME=$(/usr/libexec/java_home -v 17) + ./gradlew :shared:assembleSharedReleaseXCFramework + mkdir -p shared/build/XCFrameworks/debug + cp -R shared/build/XCFrameworks/release/shared.xcframework shared/build/XCFrameworks/debug/ + + # 3b Ensure XCFramework has Info.plist (Kotlin may not create it; Xcode requires it) + - name: Ensure XCFramework Info.plist + run: | + XCF_DIR="shared/build/XCFrameworks/debug/shared.xcframework" + if [ ! -f "$XCF_DIR/Info.plist" ]; then + echo "Creating Info.plist for XCFramework..." + cp shared/build/XCFrameworks/release/shared.xcframework/Info.plist "$XCF_DIR/" 2>/dev/null || \ + cp shared/XCFramework-Info.plist "$XCF_DIR/Info.plist" + echo "✅ Info.plist ready" + else + echo "✅ Info.plist already exists" + fi + + # 4️⃣ Setup Xcode 26.2 (macos-26 has it pre-installed) + - name: Select Xcode version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.2' + + # 4️⃣ Decode certificate (Fastlane will import it) + - name: Decode iOS certificate + run: | + # Check if the secret exists and is not empty + if [ -z "$IOS_P12_BASE64" ]; then + echo "❌ ERROR: IOS_P12_BASE64 secret is missing or empty!" + exit 1 + fi + + # Decode certificate + echo "$IOS_P12_BASE64" | base64 --decode > cert.p12 + + # Verify the .p12 file was created and has content + FILE_SIZE=$(stat -f%z cert.p12) + echo "✅ Certificate file size: $FILE_SIZE bytes" + + if [ "$FILE_SIZE" -lt 100 ]; then + echo "❌ ERROR: Certificate file is too small, likely corrupted" + exit 1 + fi + env: + IOS_P12_BASE64: ${{ secrets.IOS_P12_BASE64 }} + + # 6️⃣ Validate iOS certificate contains private key + - name: Validate iOS certificate contains private key + run: | + set +e # Don't exit on openssl failure - we need to show the error + echo "🔍 Validating p12 certificate contents..." + echo "" + + # Check if p12 file exists + if [ ! -f cert.p12 ]; then + echo "❌ ERROR: cert.p12 file not found!" + exit 1 + fi + + # Test 1: Check for PRIVATE KEY in p12 structure + # -legacy: OpenSSL 3.x on macos-26 disables RC2-40-CBC; Apple p12 often uses it + echo "📋 Test 1: Checking p12 structure for private key..." + CERT_CONTENTS=$(openssl pkcs12 -in cert.p12 -legacy -nodes -passin env:IOS_P12_PASSWORD 2>&1) + OPENSSL_EXIT=$? + set -e + + if [ $OPENSSL_EXIT -ne 0 ]; then + echo "❌ ERROR: OpenSSL failed to read p12 (exit code $OPENSSL_EXIT)" + echo "" + echo "Output: $CERT_CONTENTS" + echo "" + echo "💡 Possible causes: wrong IOS_P12_PASSWORD, or OpenSSL 3 on macos-26 (we use -legacy for Apple p12)" + exit 1 + fi + + HAS_PRIVATE_KEY=true + + if echo "$CERT_CONTENTS" | grep -q "PRIVATE KEY"; then + echo "✅ Private key found in p12 certificate structure" + else + echo "❌ ERROR: p12 certificate does NOT contain a private key in its structure!" + HAS_PRIVATE_KEY=false + fi + + # Test 2: Check if private key can be extracted (nocerts flag) - informational only + echo "" + echo "📋 Test 2: Attempting to extract private key only (nocerts)..." + if openssl pkcs12 -in cert.p12 -legacy -nocerts -passin pass:"$IOS_P12_PASSWORD" >/dev/null 2>&1; then + echo "✅ Private key can be extracted separately" + else + echo "⚠️ Note: nocerts extraction failed (this is OK if Test 1 passed)" + fi + + # Test 3: Alternative private key check using env variable - informational only + echo "" + echo "📋 Test 3: Alternative private key extraction test..." + if openssl pkcs12 -in cert.p12 -legacy -nocerts -passin env:IOS_P12_PASSWORD >/dev/null 2>&1; then + echo "✅ Alternative extraction succeeded" + else + echo "⚠️ Note: Alternative extraction failed (this is OK if Test 1 passed)" + fi + + # Test 4: Show certificate details + echo "" + echo "📋 Test 4: Certificate details (subject and validity):" + CERT_SUBJECT=$(echo "$CERT_CONTENTS" | openssl x509 -noout -subject 2>/dev/null || echo "Could not extract subject") + CERT_DATES=$(echo "$CERT_CONTENTS" | openssl x509 -noout -dates 2>/dev/null || echo "Could not extract dates") + echo "$CERT_SUBJECT" + echo "$CERT_DATES" + + # Check if this is the correct certificate (Apple Distribution) + if echo "$CERT_SUBJECT" | grep -q "Apple Distribution"; then + echo "✅ Certificate type confirmed: Apple Distribution" + else + echo "⚠️ WARNING: Certificate may not be 'Apple Distribution' type" + fi + + # Test 5: List certificate and key components (double-check private key presence) + echo "" + echo "📋 Test 5: Certificate and key components found in p12:" + COMPONENTS=$(echo "$CERT_CONTENTS" | grep -E "BEGIN|END") + echo "$COMPONENTS" + + # Final validation: Ensure private key is present in the structure + if echo "$COMPONENTS" | grep -q "BEGIN PRIVATE KEY"; then + echo "" + echo "✅ FINAL CHECK: Private key structure confirmed in p12" + HAS_PRIVATE_KEY=true + elif echo "$COMPONENTS" | grep -q "BEGIN RSA PRIVATE KEY"; then + echo "" + echo "✅ FINAL CHECK: RSA private key structure confirmed in p12" + HAS_PRIVATE_KEY=true + elif [ "${HAS_PRIVATE_KEY}" = "false" ]; then + echo "" + echo "❌ VALIDATION FAILED: p12 does not contain a valid private key" + echo "" + echo "⚠️ Possible issues:" + echo " 1. The p12 was exported without including the private key" + echo " 2. Wrong password is being used (IOS_P12_PASSWORD)" + echo " 3. The certificate file is corrupted" + echo "" + echo "💡 To fix: Export the certificate from Keychain Access ensuring:" + echo " - Expand the certificate (click the triangle/arrow)" + echo " - Select both the certificate AND its private key (you should see 2 items)" + echo " - Right-click and choose 'Export 2 items...'" + echo " - Save as .p12 format" + exit 1 + fi + + echo "" + echo "✅ All validation tests passed - p12 contains a valid private key" + echo "" + echo "📝 Note: After Fastlane imports this certificate into the keychain," + echo " additional checks will verify:" + echo " - security find-certificate (certificate in keychain)" + echo " - security find-key (private key in keychain)" + echo " - security find-identity (valid signing identity)" + env: + IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }} + + # 7️⃣ Decode provisioning profile (Fastlane will install it) + - name: Decode provisioning profile + run: | + # Check if the secret exists + if [ -z "$IOS_PROFILE_BASE64" ]; then + echo "❌ ERROR: IOS_PROFILE_BASE64 secret is missing or empty!" + exit 1 + fi + + # Decode provisioning profile + echo "$IOS_PROFILE_BASE64" | base64 --decode > profile.mobileprovision + + FILE_SIZE=$(stat -f%z profile.mobileprovision) + echo "✅ Provisioning profile size: $FILE_SIZE bytes" + env: + IOS_PROFILE_BASE64: ${{ secrets.IOS_PROFILE_BASE64 }} + + # 7️⃣b Decode GoogleService-Info.plist (Firebase config) + - name: Decode GoogleService-Info.plist + run: | + if [ -z "$GOOGLE_SERVICE_INFO_BASE64" ]; then + echo "❌ ERROR: GOOGLE_SERVICE_INFO_BASE64 secret is missing or empty!" + exit 1 + fi + echo "$GOOGLE_SERVICE_INFO_BASE64" | base64 --decode > iosApp/iosApp/GoogleService-Info.plist + FILE_SIZE=$(stat -f%z iosApp/iosApp/GoogleService-Info.plist) + echo "✅ GoogleService-Info.plist created ($FILE_SIZE bytes)" + env: + GOOGLE_SERVICE_INFO_BASE64: ${{ secrets.GOOGLE_SERVICE_INFO_BASE64 }} + + # 8️⃣ Setup App Store Connect API Key + - name: Setup App Store Connect API Key + run: | + # Check if secrets exist + if [ -z "$ASC_API_KEY_BASE64" ]; then + echo "❌ ERROR: ASC_API_KEY_BASE64 secret is missing or empty!" + exit 1 + fi + + if [ -z "${{ secrets.ASC_KEY_ID }}" ] || [ -z "${{ secrets.ASC_ISSUER_ID }}" ]; then + echo "❌ ERROR: ASC_KEY_ID or ASC_ISSUER_ID secret is missing!" + exit 1 + fi + + mkdir -p ~/.fastlane + echo "$ASC_API_KEY_BASE64" | base64 --decode > ~/.fastlane/AuthKey.p8 + chmod 600 ~/.fastlane/AuthKey.p8 + + # Verify the key file was created + if [ ! -f ~/.fastlane/AuthKey.p8 ]; then + echo "❌ ERROR: Failed to create AuthKey.p8" + exit 1 + fi + + echo "✅ App Store Connect API key configured" + env: + ASC_API_KEY_BASE64: ${{ secrets.ASC_API_KEY_BASE64 }} + + # 8b Set build number (Config.xcconfig overrides agvtool; must be > previous TestFlight build) + - name: Set build number for TestFlight + run: | + BUILD_NUM=$(date +%s) + sed -i.bak "s/CURRENT_PROJECT_VERSION=.*/CURRENT_PROJECT_VERSION=$BUILD_NUM/" iosApp/Configuration/Config.xcconfig + rm -f iosApp/Configuration/Config.xcconfig.bak + echo "Set CURRENT_PROJECT_VERSION to $BUILD_NUM" + + # 9️⃣ Build & Upload to TestFlight via Fastlane (includes verification inside) + - name: Build & Upload to TestFlight + run: bundle exec fastlane beta + env: + IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + + # 9️⃣ Upload build artifacts + - name: Upload IPA artifact + uses: actions/upload-artifact@v4 + if: success() + with: + name: ARK-Drop-${{ github.run_number }}.ipa + path: build/ARK-Drop.ipa + retention-days: 30 + + # 📋 Show build errors if build failed + - name: Show Build Log Tail + if: failure() + run: | + echo "🔍 Last 200 lines of build log:" + find build/logs -name "*.log" -exec tail -200 {} \; 2>/dev/null || echo "No build log found" + + echo "" + echo "🔍 Checking for error lines:" + find build/logs -name "*.log" -exec grep -i "error:" {} \; 2>/dev/null || echo "No errors found in log" + + # 📋 Upload build logs for debugging + - name: Upload Build Logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: xcode-build-logs + path: | + ~/Library/Logs/gym/ + build/ + retention-days: 7 + + # 🔟 Cleanup + - name: Cleanup + if: always() + run: | + # Clean up certificate and profile files + rm -f cert.p12 profile.mobileprovision ~/.fastlane/AuthKey.p8 + # Fastlane's setup_ci will clean up its own keychain automatically diff --git a/.gitignore b/.gitignore index 259e370..ab94ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,44 @@ -# JNILibs -/app/src/main/jniLibs - -# Development Setup -/build -/.idea -/.kotlin -/.gradle -/captures +*.iml +.kotlin +.gradle +**/build/ +xcuserdata +!src/**/build/ local.properties -.cxx +.idea .DS_Store +captures .externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +node_modules/ + +# Firebase (contains API keys — never commit) +GoogleService-Info.plist +google-services.json + +# Fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output +fastlane/.env* +# Certificates and profiles (NEVER commit these) +*.p12 +*.mobileprovision +*.cer +*.certSigningRequest +*.p8 + +# Bundler +vendor/bundle/ +.bundle/ +Gemfile.lock +# Generated docs (do not push) +IOS_IMPLEMENTATION_PR.md diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..40734a7 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "fastlane", "~> 2.219" +gem "cocoapods", "~> 1.15" diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts deleted file mode 100644 index 15edf56..0000000 --- a/app/build.gradle.kts +++ /dev/null @@ -1,195 +0,0 @@ -import com.android.build.gradle.internal.tasks.factory.dependsOn - -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.triplet.play) - alias(libs.plugins.ksp) - alias(libs.plugins.ktlint.gradle) - alias(libs.plugins.kotlin.serialization) -} - -kotlin { - compilerOptions { - jvmToolchain(11) - } -} - -android { - namespace = "dev.arkbuilders.drop.app" - compileSdk = 36 - - signingConfigs { - create("testRelease") { - storeFile = project.rootProject.file("keystore.jks") - storePassword = "sw0rdf1sh" - keyAlias = "ark-builders-test" - keyPassword = "rybamech" - } - } - - defaultConfig { - applicationId = "dev.arkbuilders.drop.app" - minSdk = 26 - targetSdk = 36 - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - setProperty("archivesBaseName", "ark-drop") - } - - ksp { - arg("room.schemaLocation", "$projectDir/schemas") - } - - buildTypes { - debug { - applicationIdSuffix = ".debug" - versionNameSuffix = "-debug" - isDebuggable = true - isMinifyEnabled = false - } - - release { - isMinifyEnabled = true - isShrinkResources = true - signingConfig = signingConfigs.getByName("testRelease") - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - - // Enable R8 full mode - isDebuggable = false - isJniDebuggable = false - isPseudoLocalesEnabled = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - buildFeatures { - compose = true - buildConfig = true - } - - packaging { - jniLibs.excludes.add("META-INF/AL2.0") - jniLibs.excludes.add("META-INF/LGPL2.1") - resources.excludes.addAll( - listOf( - "META-INF/DEPENDENCIES", - "META-INF/LICENSE", - "META-INF/LICENSE.txt", - "META-INF/license.txt", - "META-INF/NOTICE", - "META-INF/NOTICE.txt", - "META-INF/notice.txt", - "META-INF/ASL2.0", - "META-INF/*.kotlin_module", - ), - ) - } - - bundle { - language { - enableSplit = false - } - density { - enableSplit = true - } - abi { - enableSplit = true - } - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - - // NAVIGATION - implementation(libs.androidx.navigation.compose) - - // Bindings setup - implementation(libs.jna) { - artifact { - extension = "aar" - type = "aar" - } - } - //noinspection Aligned16KB - implementation(libs.arkbuilders.drop) { - artifact { - extension = "aar" - type = "aar" - } - } - - // QR CODE create setup - implementation(libs.google.zxing.core) - implementation(libs.github.yuriy.budiyev.code.scanner) - - // QR CODE SCAN - implementation(libs.androidx.camera.core) - implementation(libs.androidx.camera.camera2) - implementation(libs.androidx.camera.lifecycle) - implementation(libs.androidx.camera.view) - implementation(libs.mlkit.barcode.scanning) - implementation(libs.accompanist.permissions) - - implementation(libs.timber) - - implementation(libs.room.runtime) - implementation(libs.room.ktx) - ksp(libs.room.compiler) - - implementation(libs.orbit.compose) - implementation(libs.orbit.viewmodel) - - // EXTRA ICONS - implementation(libs.simple.icons) - implementation(libs.font.awesome) - implementation(libs.tabler.icons) - - // DEVELOPMENT SETUP - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) - - // File-system profile manager - implementation(libs.io.coil) - implementation(libs.androidx.compose.foundation) - implementation(libs.kotlinx.serialization) - - implementation(libs.ark.about) - - // Koin Dependency Injection - implementation(libs.io.koin.core) - implementation(libs.io.koin.android) - implementation(libs.io.koin.compose) - implementation(libs.io.koin.test) -} - -tasks.preBuild.dependsOn(tasks.ktlintCheck) -tasks.ktlintCheck.dependsOn(tasks.ktlintFormat) - -tasks.named("clean") { - delete(fileTree("$projectDir/src/main/jniLibs")) -} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index 5b8e65c..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,135 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. --keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. --renamesourcefileattribute SourceFile - -# Keep Hilt classes --keep class dagger.hilt.** { *; } --keep class javax.inject.** { *; } --keep class * extends dagger.hilt.android.HiltAndroidApp --keepclasseswithmembers class * { - @dagger.hilt.android.AndroidEntryPoint ; -} - -# Keep Compose classes --keep class androidx.compose.** { *; } --keep class kotlin.Metadata { *; } - -# Keep serialization classes --keepattributes *Annotation*, InnerClasses --dontnote kotlinx.serialization.AnnotationsKt --keepclassmembers class kotlinx.serialization.json.** { - *** Companion; -} --keepclasseswithmembers class kotlinx.serialization.json.** { - kotlinx.serialization.KSerializer serializer(...); -} --keep,includedescriptorclasses class dev.arkbuilders.drop.app.**$$serializer { *; } --keepclassmembers class dev.arkbuilders.drop.app.** { - *** Companion; -} --keepclasseswithmembers class dev.arkbuilders.drop.app.** { - kotlinx.serialization.KSerializer serializer(...); -} - -# Keep JNA classes --keep class com.sun.jna.** { *; } --keep class * implements com.sun.jna.** { *; } - -# Keep ZXing classes but exclude desktop GUI components --keep class com.google.zxing.** { *; } --dontwarn com.google.zxing.client.j2se.** --dontwarn java.awt.** --dontwarn javax.swing.** --dontwarn javax.imageio.** --dontwarn org.w3c.dom.bootstrap.** - -# Exclude ZXing desktop GUI classes completely --dontnote com.google.zxing.client.j2se.** - -# Keep CameraX classes --keep class androidx.camera.** { *; } - -# Keep ML Kit classes --keep class com.google.mlkit.** { *; } - -# Keep file provider classes --keep class androidx.core.content.FileProvider { *; } - -# Remove logging in release --assumenosideeffects class android.util.Log { - public static boolean isLoggable(java.lang.String, int); - public static int v(...); - public static int i(...); - public static int w(...); - public static int d(...); - public static int e(...); -} - -# Fix for missing javax.imageio classes from ZXing --dontwarn javax.imageio.spi.ImageInputStreamSpi --dontwarn javax.imageio.spi.ImageOutputStreamSpi --dontwarn javax.imageio.spi.ImageReaderSpi --dontwarn javax.imageio.spi.ImageWriterSpi --dontwarn com.github.jaiimageio.impl.** - -# Fix for missing AWT classes from ZXing desktop components --dontwarn java.awt.Component --dontwarn java.awt.Container --dontwarn java.awt.Dimension --dontwarn java.awt.FlowLayout --dontwarn java.awt.Graphics2D --dontwarn java.awt.GraphicsEnvironment --dontwarn java.awt.HeadlessException --dontwarn java.awt.Image --dontwarn java.awt.LayoutManager --dontwarn java.awt.Window --dontwarn java.awt.geom.AffineTransform --dontwarn java.awt.image.BufferedImage --dontwarn java.awt.image.ImageObserver --dontwarn java.awt.image.RenderedImage --dontwarn java.awt.image.WritableRaster - -# Fix for missing Swing classes from ZXing desktop components --dontwarn javax.swing.Icon --dontwarn javax.swing.ImageIcon --dontwarn javax.swing.JFileChooser --dontwarn javax.swing.JFrame --dontwarn javax.swing.JLabel --dontwarn javax.swing.JPanel --dontwarn javax.swing.JTextArea --dontwarn javax.swing.SwingUtilities --dontwarn javax.swing.text.JTextComponent - -# Suppress warnings for ZXing desktop classes that we don't use on Android --dontwarn com.google.zxing.client.j2se.GUIRunner --dontwarn com.google.zxing.client.j2se.BufferedImageLuminanceSource --dontwarn com.google.zxing.client.j2se.DecodeWorker --dontwarn com.google.zxing.client.j2se.HtmlAssetTranslator - -# Keep only the ZXing classes we actually use for Android --keep class com.google.zxing.BarcodeFormat { *; } --keep class com.google.zxing.WriterException { *; } --keep class com.google.zxing.common.BitMatrix { *; } --keep class com.google.zxing.qrcode.QRCodeWriter { *; } - -# Additional R8 optimizations --allowaccessmodification --repackageclasses '' diff --git a/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt b/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt deleted file mode 100644 index 2d5d626..0000000 --- a/app/src/androidTest/java/dev/arkbuilders/drop/app/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package dev.arkbuilders.drop.app - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Assert.assertEquals("dev.arkbuilders.drop", appContext.packageName) - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt deleted file mode 100644 index f0962da..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/datasource/ProfileLocalDataSource.kt +++ /dev/null @@ -1,84 +0,0 @@ -package dev.arkbuilders.drop.app.data.datasource - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import dev.arkbuilders.drop.app.data.model.UserAvatarDto -import dev.arkbuilders.drop.app.data.model.UserProfileDto -import dev.arkbuilders.drop.app.domain.AvatarHelper -import dev.arkbuilders.drop.app.domain.model.UserAvatar -import dev.arkbuilders.drop.app.domain.model.UserProfile -import kotlinx.serialization.json.Json - -class ProfileLocalDataSource( - context: Context, - private val avatarHelper: AvatarHelper, -) { - private val prefs: SharedPreferences = - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - - private val json = Json { ignoreUnknownKeys = true } - - fun loadProfile(): UserProfile { - val profileJson = prefs.getString(KEY_PROFILE, null) ?: return createDefaultProfile() - - return runCatching { - json - .decodeFromString(profileJson) - .toDomain() - }.getOrElse { - createDefaultProfile() - } - } - - private fun createDefaultProfile(): UserProfile { - val defaultAvatarId = "avatar_00" - val default = - UserProfile( - name = "Anonymous", - avatar = - UserAvatar( - base64 = avatarHelper.getDefaultAvatarBase64(defaultAvatarId), - predefinedId = defaultAvatarId, - ), - ) - saveProfile(default) - return default - } - - fun saveProfile(profile: UserProfile) { - runCatching { - val profileJson = json.encodeToString(profile.toDto()) - prefs.edit { putString(KEY_PROFILE, profileJson) } - } - } - - companion object { - private const val PREFS_NAME = "drop_profile" - private const val KEY_PROFILE = "user_profile" - } -} - -private fun UserProfileDto.toDomain() = - UserProfile( - name = name, - avatar = avatar.toDomain(), - ) - -private fun UserProfile.toDto() = - UserProfileDto( - name = name, - avatar = avatar.toDto(), - ) - -private fun UserAvatar.toDto() = - UserAvatarDto( - base64 = base64, - predefinedId = predefinedId, - ) - -private fun UserAvatarDto.toDomain() = - UserAvatar( - base64 = base64, - predefinedId = predefinedId, - ) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt deleted file mode 100644 index 4b87b2c..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/Database.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.arkbuilders.drop.app.data.db - -import android.content.Context -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import dev.arkbuilders.drop.app.data.db.dao.TransferSessionDao -import dev.arkbuilders.drop.app.data.db.entity.RoomTransferSession -import dev.arkbuilders.drop.app.data.db.typeconverters.DropFileListConverter -import dev.arkbuilders.drop.app.data.db.typeconverters.OffsetDateTimeTypeConverter - -@androidx.room.Database( - entities = [ - RoomTransferSession::class, - ], - version = 1, - exportSchema = true, -) -@TypeConverters( - OffsetDateTimeTypeConverter::class, - DropFileListConverter::class, -) -abstract class Database : RoomDatabase() { - abstract fun transferHistoryDao(): TransferSessionDao - - companion object { - const val DB_NAME = "arkdrop.db" - - fun build(ctx: Context) = - Room.databaseBuilder(ctx, Database::class.java, DB_NAME) - .build() - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt deleted file mode 100644 index b91bab6..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/db/typeconverters/OffsetDateTimeTypeConverter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.arkbuilders.drop.app.data.db.typeconverters - -import androidx.room.TypeConverter -import java.time.OffsetDateTime - -object OffsetDateTimeTypeConverter { - @TypeConverter - fun fromOffsetDateTime(date: OffsetDateTime): String = date.toString() - - @TypeConverter - fun toOffsetDateTime(dateString: String): OffsetDateTime { - val date = OffsetDateTime.parse(dateString) - val offset = OffsetDateTime.now().offset - return date.withOffsetSameInstant(offset) - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt deleted file mode 100644 index cdafc8e..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserAvatarDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.arkbuilders.drop.app.data.model - -import kotlinx.serialization.Serializable - -@Serializable -data class UserAvatarDto( - val base64: String, - val predefinedId: String?, -) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt deleted file mode 100644 index 5688139..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/model/UserProfileDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.arkbuilders.drop.app.data.model - -import kotlinx.serialization.Serializable - -@Serializable -data class UserProfileDto( - val name: String, - val avatar: UserAvatarDto, -) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt deleted file mode 100644 index 165c968..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ProfileRepoImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.arkbuilders.drop.app.data.repository - -import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource -import dev.arkbuilders.drop.app.domain.model.UserAvatar -import dev.arkbuilders.drop.app.domain.model.UserProfile -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -class ProfileRepoImpl( - private val localDataSource: ProfileLocalDataSource, -) : ProfileRepo { - private val _profile = MutableStateFlow(localDataSource.loadProfile()) - override val profile: StateFlow = _profile.asStateFlow() - - override fun updateProfile(profile: UserProfile) { - _profile.value = profile - localDataSource.saveProfile(profile) - } - - override fun updateName(name: String) { - updateProfile(_profile.value.copy(name = name)) - } - - override fun updateAvatar(avatar: UserAvatar) { - updateProfile(_profile.value.copy(avatar = avatar)) - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt deleted file mode 100644 index cacc4f2..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/ReceiveSessionRepo.kt +++ /dev/null @@ -1,119 +0,0 @@ -package dev.arkbuilders.drop.app.data.repository - -import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl -import dev.arkbuilders.drop.app.domain.ResourcesHelper -import dev.arkbuilders.drop.app.domain.model.DropFileInfo -import dev.arkbuilders.drop.app.domain.model.ReceiveSession -import dev.arkbuilders.drop.app.domain.model.TransferStatus -import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo -import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import timber.log.Timber - -class ReceiveSessionRepo( - private val receiveFilesUseCase: ReceiveFilesUseCase, - private val transferHistoryRepository: TransferSessionRepo, - private val resourcesHelper: ResourcesHelper, -) { - // Keep references to active sessions here so file transfers continue even if the ViewModel dies - private val activeSessions = mutableListOf() - private val activeSessionsMutex = Mutex() - private val cancelScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - suspend fun receiveFiles( - ticket: String, - confirmation: UByte, - ): ReceiveSession? = - withContext(Dispatchers.IO) { - receiveFilesUseCase.invoke(ticket, confirmation).fold( - onSuccess = { bubble -> - val subscriber = - ReceiveFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - - val session = - ReceiveSession( - bubble, - subscriber, - ) - activeSessionsMutex.withLock { - activeSessions.add(session) - } - - bubble.start() - return@withContext session - }, - onFailure = { - return@withContext null - }, - ) - } - - suspend fun saveReceivedFiles(session: ReceiveSession): List = - withContext(Dispatchers.IO) { - val subscriber = session.subscriber - val completeFiles = subscriber.getCompleteFiles() - val savedFiles = mutableListOf() - - try { - completeFiles.forEach { (fileInfo, data) -> - val savedFile = resourcesHelper.saveFileToDownloads(fileInfo.name, data) - if (savedFile != null) { - savedFiles.add(DropFileInfo(savedFile, fileInfo.size.toLong())) - Timber.i("Saved file name: $savedFile") - } else { - Timber.e("Failed to save file: ${fileInfo.name}") - } - } - - if (savedFiles.isNotEmpty()) { - val progress = subscriber.progress.value - val senderName = progress.senderName - val senderAvatar = progress.senderAvatar - - transferHistoryRepository.addReceivedTransfer( - files = savedFiles, - peerName = senderName, - peerAvatar = senderAvatar, - status = TransferStatus.COMPLETED, - ) - } - } catch (e: Exception) { - Timber.e("Error saving received files ${e.message}") - - val progress = subscriber.progress.value - val senderName = progress.senderName - val senderAvatar = progress.senderAvatar - - transferHistoryRepository.addReceivedTransfer( - files = emptyList(), - peerName = senderName, - peerAvatar = senderAvatar, - status = TransferStatus.FAILED, - ) - } - - return@withContext savedFiles.map { it.name } - } - - fun cancelReceive(session: ReceiveSession) { - cancelScope.launch { - try { - activeSessionsMutex.withLock { - activeSessions.remove(session) - } - session.bubble.unsubscribe(session.subscriber) - session.bubble.cancel() - } catch (e: Throwable) { - Timber.e("Error cancelling receive ${e.message}") - } - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt deleted file mode 100644 index 07beed7..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/data/repository/SendSessionRepo.kt +++ /dev/null @@ -1,100 +0,0 @@ -package dev.arkbuilders.drop.app.data.repository - -import android.net.Uri -import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl -import dev.arkbuilders.drop.app.domain.ResourcesHelper -import dev.arkbuilders.drop.app.domain.model.DropFileInfo -import dev.arkbuilders.drop.app.domain.model.SendSession -import dev.arkbuilders.drop.app.domain.model.TransferStatus -import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo -import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import timber.log.Timber - -class SendSessionRepo( - private val sendUseCase: SendFilesUseCase, - private val resourcesHelper: ResourcesHelper, - private val transferSessionRepository: TransferSessionRepo, -) { - // Keep references to active sessions here so file transfers continue even if the ViewModel dies - private val activeSessions = mutableListOf() - private val activeSessionsMutex = Mutex() - private val cancelScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - suspend fun sendFiles(fileUris: List): SendSession? = - withContext(Dispatchers.IO) { - cleanupFinishedSessions() - - sendUseCase.invoke(fileUris).fold( - onSuccess = { bubble -> - val subscriber = - SendFilesSubscriberImpl().also { subscriber -> - bubble.subscribe(subscriber) - } - - val session = SendSession(bubble, subscriber) - activeSessionsMutex.withLock { - activeSessions.add(session) - } - return@withContext session - }, - onFailure = { - return@withContext null - }, - ) - } - - suspend fun recordSendCompletion( - fileUris: List, - session: SendSession, - ) { - try { - cleanupFinishedSessions() - val progress = session.subscriber.progress.value - val receiverName = progress.receiverName - val receiverAvatar = progress.receiverAvatar - - val filesInfo = - fileUris.map { - DropFileInfo( - name = resourcesHelper.getFileName(it.toString()) ?: "", - size = resourcesHelper.getFileSize(it.toString()), - ) - } - - transferSessionRepository.addSentTransfer( - files = filesInfo, - peerName = receiverName, - peerAvatar = receiverAvatar, - status = TransferStatus.COMPLETED, - ) - } catch (e: Exception) { - Timber.e("Error recording send completion ${e.message}") - } - } - - fun cancelSend(session: SendSession) { - cancelScope.launch { - try { - activeSessionsMutex.withLock { - activeSessions.remove(session) - } - session.bubble.unsubscribe(session.subscriber) - session.bubble.cancel() - } catch (e: Throwable) { - Timber.e("Error cancelling send ${e.message}") - } - } - } - - private suspend fun cleanupFinishedSessions() = - activeSessionsMutex.withLock { - activeSessions.removeAll { it.bubble.isFinished() } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt deleted file mode 100644 index e5b71bc..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.arkbuilders.drop.app.di - -import dev.arkbuilders.drop.app.data.datasource.ProfileLocalDataSource -import dev.arkbuilders.drop.app.data.datasource.TransferSessionLocalDataSource -import dev.arkbuilders.drop.app.data.db.Database -import dev.arkbuilders.drop.app.data.db.dao.TransferSessionDao -import dev.arkbuilders.drop.app.data.helper.AvatarHelperImpl -import dev.arkbuilders.drop.app.data.helper.PermissionsHelperImpl -import dev.arkbuilders.drop.app.data.helper.ResourcesHelperImpl -import dev.arkbuilders.drop.app.data.repository.NetworkStatusImpl -import dev.arkbuilders.drop.app.data.repository.ProfileRepoImpl -import dev.arkbuilders.drop.app.data.repository.ReceiveSessionRepo -import dev.arkbuilders.drop.app.data.repository.SendSessionRepo -import dev.arkbuilders.drop.app.data.repository.TransferSessionRepoImpl -import dev.arkbuilders.drop.app.domain.AvatarHelper -import dev.arkbuilders.drop.app.domain.PermissionsHelper -import dev.arkbuilders.drop.app.domain.ResourcesHelper -import dev.arkbuilders.drop.app.domain.repository.NetworkStatus -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo -import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase -import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase -import org.koin.dsl.module - -val appModule = - module { - single { ProfileRepoImpl(get()) } - single { ResourcesHelperImpl(get()) } - single { Database.build(get()) } - single { TransferSessionRepoImpl(get()) } - single { PermissionsHelperImpl(get()) } - single { NetworkStatusImpl(get()) } - single { AvatarHelperImpl(get()) } - single { ProfileLocalDataSource(get(), get()) } - single { TransferSessionLocalDataSource(get()) } - single { SendSessionRepo(get(), get(), get()) } - single { ReceiveSessionRepo(get(), get(), get()) } - factory { - val db: Database = get() - db.transferHistoryDao() - } - factory { SendFilesUseCase(get(), get(), get()) } - factory { ReceiveFilesUseCase(get()) } - } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt b/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt deleted file mode 100644 index ab8f0e8..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/di/ViewModelsModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.arkbuilders.drop.app.di - -import dev.arkbuilders.drop.app.presentation.history.HistoryViewModel -import dev.arkbuilders.drop.app.presentation.home.HomeViewModel -import dev.arkbuilders.drop.app.presentation.profile.EditProfileViewModel -import dev.arkbuilders.drop.app.presentation.receive.ReceiveViewModel -import dev.arkbuilders.drop.app.presentation.send.SendViewModel -import org.koin.core.module.dsl.viewModel -import org.koin.dsl.module - -val viewModelsModule = - module { - viewModel { HistoryViewModel(get()) } - viewModel { HomeViewModel(get(), get(), get()) } - viewModel { EditProfileViewModel(get(), get()) } - viewModel { ReceiveViewModel(get(), get()) } - viewModel { SendViewModel(get(), get(), get()) } - } diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt deleted file mode 100644 index 91ec9f5..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/AvatarHelper.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.arkbuilders.drop.app.domain - -interface AvatarHelper { - fun uriToBase64(uri: String): String? - - fun getDefaultAvatarBase64(avatarId: String): String - - companion object { - const val MAX_IMAGE_SIZE = 512 // Maximum width/height in pixels - const val JPEG_QUALITY = 85 // JPEG compression quality - const val MAX_FILE_SIZE = 500 * 1024 // 500KB max file size - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt deleted file mode 100644 index d730dd7..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/ResourcesHelper.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.arkbuilders.drop.app.domain - -interface ResourcesHelper { - fun getFileName(uri: String): String? - - fun validateUris(uris: List): Pair, Int> - - fun getFileSize(uri: String): Long - - fun saveFileToDownloads( - fileName: String, - data: ByteArray, - ): String? -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt deleted file mode 100644 index 1e139a1..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/BuildConfigFields.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.arkbuilders.drop.app.domain.model - -class BuildConfigFields( - val versionCode: Int, - val versionName: String, -) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt deleted file mode 100644 index 4f8e7b8..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/ReceiveSession.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.arkbuilders.drop.app.domain.model - -import dev.arkbuilders.drop.ReceiveFilesBubble -import dev.arkbuilders.drop.app.data.ReceiveFilesSubscriberImpl - -class ReceiveSession( - val bubble: ReceiveFilesBubble, - val subscriber: ReceiveFilesSubscriberImpl, -) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt deleted file mode 100644 index 25208bb..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/model/SendSession.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.arkbuilders.drop.app.domain.model - -import dev.arkbuilders.drop.SendFilesBubble -import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl - -class SendSession( - val bubble: SendFilesBubble, - val subscriber: SendFilesSubscriberImpl, -) diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt deleted file mode 100644 index 804c89b..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/NetworkStatus.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.arkbuilders.drop.app.domain.repository - -import kotlinx.coroutines.flow.StateFlow - -interface NetworkStatus { - fun isOnline() = onlineStatus.value - - val onlineStatus: StateFlow -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt deleted file mode 100644 index 960dddf..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/repository/ProfileRepo.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.arkbuilders.drop.app.domain.repository - -import dev.arkbuilders.drop.app.domain.model.UserAvatar -import dev.arkbuilders.drop.app.domain.model.UserProfile -import kotlinx.coroutines.flow.StateFlow - -interface ProfileRepo { - val profile: StateFlow - - fun getCurrentProfile() = profile.value - - fun updateProfile(profile: UserProfile) - - fun updateName(name: String) - - fun updateAvatar(avatar: UserAvatar) -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt deleted file mode 100644 index 732d522..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/ReceiveFilesUseCase.kt +++ /dev/null @@ -1,51 +0,0 @@ -package dev.arkbuilders.drop.app.domain.usecase - -import dev.arkbuilders.drop.ReceiveFilesBubble -import dev.arkbuilders.drop.ReceiveFilesRequest -import dev.arkbuilders.drop.ReceiverConfig -import dev.arkbuilders.drop.ReceiverProfile -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.receiveFiles -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber - -class ReceiveFilesUseCase( - private val profileRepo: ProfileRepo, -) { - suspend operator fun invoke( - ticket: String, - confirmation: UByte, - ): Result = - withContext(Dispatchers.IO) { - runCatching { - Timber.d("Starting file receive with ticket: $ticket") - - val profile = profileRepo.getCurrentProfile() - val receiverProfile = - ReceiverProfile( - name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() }, - ) - - val request = - ReceiveFilesRequest( - ticket = ticket, - confirmation = confirmation, - profile = receiverProfile, - config = - ReceiverConfig( - chunkSize = 1024u * 512u, - parallelStreams = 4u, - ), - ) - - val bubble = receiveFiles(request) - - Timber.d("Receive bubble created and started") - bubble - }.onFailure { - Timber.e("Error starting file receive ${it.message}") - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt b/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt deleted file mode 100644 index 3cb1638..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/domain/usecase/SendFilesUseCase.kt +++ /dev/null @@ -1,78 +0,0 @@ -package dev.arkbuilders.drop.app.domain.usecase - -import android.content.Context -import android.net.Uri -import dev.arkbuilders.drop.SendFilesBubble -import dev.arkbuilders.drop.SendFilesRequest -import dev.arkbuilders.drop.SenderConfig -import dev.arkbuilders.drop.SenderFile -import dev.arkbuilders.drop.SenderProfile -import dev.arkbuilders.drop.app.data.SenderFileDataImpl -import dev.arkbuilders.drop.app.domain.ResourcesHelper -import dev.arkbuilders.drop.app.domain.repository.ProfileRepo -import dev.arkbuilders.drop.sendFiles -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber - -class SendFilesUseCase( - private val context: Context, - private val profileRepo: ProfileRepo, - private val resourcesHelper: ResourcesHelper, -) { - suspend operator fun invoke(fileUris: List): Result = - withContext(Dispatchers.IO) { - runCatching { - Timber.d("Starting file send for ${fileUris.size} files") - - val profile = profileRepo.getCurrentProfile() - val senderProfile = - SenderProfile( - name = profile.name.ifEmpty { "Anonymous" }, - avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() }, - ) - - val senderFiles = - fileUris.mapNotNull { uri -> - val fileName = resourcesHelper.getFileName(uri.toString()) - if (fileName != null) { - val fileData = SenderFileDataImpl(context, uri) - SenderFile( - name = fileName, - data = fileData, - ) - } else { - Timber.w("Could not get filename for URI: $uri") - null - } - } - - if (senderFiles.isEmpty()) { - Timber.e("No valid files to send") - error("No valid files to send") - } - - val request = - SendFilesRequest( - profile = senderProfile, - files = senderFiles, - config = - SenderConfig( - chunkSize = 1024u * 512u, - parallelStreams = 4u, - ), - ) - - val bubble = sendFiles(request) - - Timber.d( - "Send bubble created with ticket and confirmation: ${ - bubble.getTicket() - } ${bubble.getConfirmation()}", - ) - bubble - }.onFailure { - Timber.e("Error starting file send ${it.message}") - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt b/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt deleted file mode 100644 index bee919f..0000000 --- a/app/src/main/java/dev/arkbuilders/drop/app/presentation/DropApplication.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.arkbuilders.drop.app.presentation - -import android.app.Application -import dev.arkbuilders.drop.app.BuildConfig -import dev.arkbuilders.drop.app.di.appModule -import dev.arkbuilders.drop.app.di.viewModelsModule -import dev.arkbuilders.drop.app.domain.model.BuildConfigFields -import org.koin.android.ext.koin.androidContext -import org.koin.core.context.startKoin -import org.koin.dsl.module -import timber.log.Timber - -class DropApplication : Application() { - override fun onCreate() { - super.onCreate() - Timber.plant(Timber.DebugTree()) - - val buildConfigFieldsModule = - module { - single { - BuildConfigFields( - BuildConfig.VERSION_CODE, - BuildConfig.VERSION_NAME, - ) - } - } - startKoin { - androidContext(this@DropApplication) - modules(appModule, viewModelsModule, buildConfigFieldsModule) - } - } -} diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 80ee670..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index 6a8f9bf..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 00afadf..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 6e542fe..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index c569ae5..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index cf0abd1..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 3cb1f6e..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index ea8abc0..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index b25f486..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9c4aa6c..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index 0a26eb8..0000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Drop - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index 630d594..0000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - -