diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 407ebcc2..8b0036ca 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -32,14 +32,14 @@ jobs: - name: Build libvpx run: cd presentation/src/main/cpp && ./build.sh - - name: Build Official Debug APK - run: ./gradlew assembleOfficialDebug + - name: Build Official Libre Debug APK + run: ./gradlew assembleOfficialLibreDebug - - name: Run Official Debug Unit Tests - run: ./gradlew testOfficialDebugUnitTest + - name: Run Official Libre Debug Unit Tests + run: ./gradlew testOfficialLibreDebugUnitTest - - name: Build Telemt Debug APK - run: ./gradlew assembleTelemtDebug + - name: Build Telemt Libre Debug APK + run: ./gradlew assembleTelemtLibreDebug - - name: Run Telemt Debug Unit Tests - run: ./gradlew testTelemtDebugUnitTest + - name: Run Telemt Libre Debug Unit Tests + run: ./gradlew testTelemtLibreDebugUnitTest diff --git a/README.md b/README.md index 59ed1084..887674b7 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,10 @@ RELEASE_KEY_PASSWORD=your_key_password ### 3. Configure Push Notifications +This step is required for `firebase` build variants. You can skip it if you only plan to build +`libre` +variants. + 1. Log in to the [Firebase console](https://console.firebase.google.com). 2. Create a new project. 3. Add two Firebase Android apps: @@ -180,26 +184,40 @@ interactively. Use these variants in Android Studio: -- `officialDebug` -- `officialRelease` -- `telemtDebug` -- `telemtRelease` +- `officialFirebaseDebug` +- `officialFirebaseRelease` +- `officialLibreDebug` +- `officialLibreRelease` +- `telemtFirebaseDebug` +- `telemtFirebaseRelease` +- `telemtLibreDebug` +- `telemtLibreRelease` + +Variant naming: + +- `official` / `telemt` selects the TDLib source +- `firebase` enables FCM / Firebase-backed push setup +- `libre` builds without Firebase dependencies Useful Gradle tasks: ```bash -./gradlew assembleOfficialReleaseTdlibApks -./gradlew assembleTelemtReleaseTdlibApks -./gradlew assembleAllReleaseTdlibApks -./gradlew assembleOfficialDebugTdlibApks -./gradlew assembleTelemtDebugTdlibApks -./gradlew assembleAllDebugTdlibApks +./gradlew :app:assembleOfficialFirebaseRelease +./gradlew :app:assembleTelemtFirebaseRelease +./gradlew :app:assembleOfficialFirebaseDebug +./gradlew :app:assembleTelemtFirebaseDebug +./gradlew :app:assembleOfficialLibreRelease +./gradlew :app:assembleTelemtLibreRelease +./gradlew :app:assembleOfficialLibreDebug +./gradlew :app:assembleTelemtLibreDebug ``` APK names: -- regular TDLib: `monogram-arm64-v8a--release.apk` -- Telemt TDLib: `monogram-telemt-arm64-v8a--release.apk` +- official Firebase: `monogram-arm64-v8a--release.apk` +- official libre: `monogram-libre-arm64-v8a--release.apk` +- Telemt Firebase: `monogram-telemt-arm64-v8a--release.apk` +- Telemt libre: `monogram-telemt-libre-arm64-v8a--release.apk` --- diff --git a/README_ES.md b/README_ES.md index ddcda62c..44a1a565 100644 --- a/README_ES.md +++ b/README_ES.md @@ -116,6 +116,9 @@ RELEASE_KEY_PASSWORD=your_key_password ### 3. Configurar notificaciones +Este paso es necesario para los variants `firebase`. Si solo planeas compilar variants `libre`, +puedes omitirlo. + 1. Inicia sesión en la [consola de Firebase](https://console.firebase.google.com). 2. Crea un nuevo proyecto. @@ -199,26 +202,40 @@ Si lo ejecutas sin argumentos, te pedirá elegir una opción. En Android Studio usa estos variants: -- `officialDebug` -- `officialRelease` -- `telemtDebug` -- `telemtRelease` +- `officialFirebaseDebug` +- `officialFirebaseRelease` +- `officialLibreDebug` +- `officialLibreRelease` +- `telemtFirebaseDebug` +- `telemtFirebaseRelease` +- `telemtLibreDebug` +- `telemtLibreRelease` + +Nombres de variants: + +- `official` / `telemt` selecciona la fuente de TDLib +- `firebase` habilita FCM / push con Firebase +- `libre` compila sin dependencias de Firebase Tareas útiles de Gradle: ```bash -./gradlew assembleOfficialReleaseTdlibApks -./gradlew assembleTelemtReleaseTdlibApks -./gradlew assembleAllReleaseTdlibApks -./gradlew assembleOfficialDebugTdlibApks -./gradlew assembleTelemtDebugTdlibApks -./gradlew assembleAllDebugTdlibApks +./gradlew :app:assembleOfficialFirebaseRelease +./gradlew :app:assembleTelemtFirebaseRelease +./gradlew :app:assembleOfficialFirebaseDebug +./gradlew :app:assembleTelemtFirebaseDebug +./gradlew :app:assembleOfficialLibreRelease +./gradlew :app:assembleTelemtLibreRelease +./gradlew :app:assembleOfficialLibreDebug +./gradlew :app:assembleTelemtLibreDebug ``` Nombres de APK: -- TDLib normal: `monogram-arm64-v8a--release.apk` -- TDLib Telemt: `monogram-telemt-arm64-v8a--release.apk` +- official Firebase: `monogram-arm64-v8a--release.apk` +- official libre: `monogram-libre-arm64-v8a--release.apk` +- Telemt Firebase: `monogram-telemt-arm64-v8a--release.apk` +- Telemt libre: `monogram-telemt-libre-arm64-v8a--release.apk` --- diff --git a/README_KOR.md b/README_KOR.md index b2b9a473..772267d1 100644 --- a/README_KOR.md +++ b/README_KOR.md @@ -102,6 +102,9 @@ RELEASE_KEY_PASSWORD=your_key_password ### 3. 푸시 알림 설정 +이 단계는 `firebase` 빌드 variants에 필요합니다. `libre` variants만 빌드할 계획이라면 +건너뛰어도 됩니다. + 1. [Firebase Console](https://console.firebase.google.com)에 로그인합니다. 2. 새 프로젝트를 생성합니다. 3. Firebase에 Android 앱 두 개를 추가합니다: @@ -178,26 +181,40 @@ sudo apt-get install build-essential git curl wget php perl gperf unzip zip defa Android Studio에서는 다음 variants를 사용하세요: -- `officialDebug` -- `officialRelease` -- `telemtDebug` -- `telemtRelease` +- `officialFirebaseDebug` +- `officialFirebaseRelease` +- `officialLibreDebug` +- `officialLibreRelease` +- `telemtFirebaseDebug` +- `telemtFirebaseRelease` +- `telemtLibreDebug` +- `telemtLibreRelease` + +Variant 이름: + +- `official` / `telemt`는 TDLib 소스를 선택합니다 +- `firebase`는 FCM / Firebase 기반 푸시를 활성화합니다 +- `libre`는 Firebase 의존성 없이 빌드합니다 유용한 Gradle 작업: ```bash -./gradlew assembleOfficialReleaseTdlibApks -./gradlew assembleTelemtReleaseTdlibApks -./gradlew assembleAllReleaseTdlibApks -./gradlew assembleOfficialDebugTdlibApks -./gradlew assembleTelemtDebugTdlibApks -./gradlew assembleAllDebugTdlibApks +./gradlew :app:assembleOfficialFirebaseRelease +./gradlew :app:assembleTelemtFirebaseRelease +./gradlew :app:assembleOfficialFirebaseDebug +./gradlew :app:assembleTelemtFirebaseDebug +./gradlew :app:assembleOfficialLibreRelease +./gradlew :app:assembleTelemtLibreRelease +./gradlew :app:assembleOfficialLibreDebug +./gradlew :app:assembleTelemtLibreDebug ``` APK 이름: -- 일반 TDLib: `monogram-arm64-v8a--release.apk` -- Telemt TDLib: `monogram-telemt-arm64-v8a--release.apk` +- official Firebase: `monogram-arm64-v8a--release.apk` +- official libre: `monogram-libre-arm64-v8a--release.apk` +- Telemt Firebase: `monogram-telemt-arm64-v8a--release.apk` +- Telemt libre: `monogram-telemt-libre-arm64-v8a--release.apk` --- diff --git a/README_RU.md b/README_RU.md index ff79c6be..fcc58ba2 100644 --- a/README_RU.md +++ b/README_RU.md @@ -103,6 +103,9 @@ RELEASE_KEY_PASSWORD=your_key_password ### 3. Настройка push-уведомлений +Этот шаг нужен для вариантов сборки `firebase`. Если вы собираете только `libre`-варианты, его можно +пропустить. + 1. Войдите в [консоль Firebase](https://console.firebase.google.com). 2. Создайте новый проект. 3. Добавьте в Firebase два Android-приложения: @@ -180,26 +183,40 @@ sudo apt-get install build-essential git curl wget php perl gperf unzip zip defa В Android Studio используйте варианты: -- `officialDebug` -- `officialRelease` -- `telemtDebug` -- `telemtRelease` +- `officialFirebaseDebug` +- `officialFirebaseRelease` +- `officialLibreDebug` +- `officialLibreRelease` +- `telemtFirebaseDebug` +- `telemtFirebaseRelease` +- `telemtLibreDebug` +- `telemtLibreRelease` + +Именование вариантов: + +- `official` / `telemt` выбирает источник TDLib +- `firebase` включает FCM / Firebase push +- `libre` собирается без Firebase-зависимостей Полезные Gradle-задачи: ```bash -./gradlew assembleOfficialReleaseTdlibApks -./gradlew assembleTelemtReleaseTdlibApks -./gradlew assembleAllReleaseTdlibApks -./gradlew assembleOfficialDebugTdlibApks -./gradlew assembleTelemtDebugTdlibApks -./gradlew assembleAllDebugTdlibApks +./gradlew :app:assembleOfficialFirebaseRelease +./gradlew :app:assembleTelemtFirebaseRelease +./gradlew :app:assembleOfficialFirebaseDebug +./gradlew :app:assembleTelemtFirebaseDebug +./gradlew :app:assembleOfficialLibreRelease +./gradlew :app:assembleTelemtLibreRelease +./gradlew :app:assembleOfficialLibreDebug +./gradlew :app:assembleTelemtLibreDebug ``` Имена APK: -- обычный TDLib: `monogram-arm64-v8a--release.apk` -- Telemt TDLib: `monogram-telemt-arm64-v8a--release.apk` +- official Firebase: `monogram-arm64-v8a--release.apk` +- official libre: `monogram-libre-arm64-v8a--release.apk` +- Telemt Firebase: `monogram-telemt-arm64-v8a--release.apk` +- Telemt libre: `monogram-telemt-libre-arm64-v8a--release.apk` --- diff --git a/README_TR.md b/README_TR.md index 34a4438e..a76f7598 100644 --- a/README_TR.md +++ b/README_TR.md @@ -100,6 +100,10 @@ RELEASE_KEY_PASSWORD=your_key_password ``` ### 3. Anlık Bildirimleri (Push Notifications) Yapılandırın +Bu adım `firebase` derleme varyantları için gereklidir. Yalnızca `libre` varyantlarını +derleyecekseniz, +bunu atlayabilirsiniz. + 1. [Firebase konsolunda](https://console.firebase.google.com) oturum açın. 2. Yeni bir proje oluşturun. 3. İki Firebase Android uygulaması ekleyin: @@ -177,26 +181,40 @@ Argümansız çalıştırırsanız, script size seçim sorar. Android Studio'da şu variantları kullanın: -- `officialDebug` -- `officialRelease` -- `telemtDebug` -- `telemtRelease` +- `officialFirebaseDebug` +- `officialFirebaseRelease` +- `officialLibreDebug` +- `officialLibreRelease` +- `telemtFirebaseDebug` +- `telemtFirebaseRelease` +- `telemtLibreDebug` +- `telemtLibreRelease` + +Variant adlandırması: + +- `official` / `telemt` TDLib kaynağını seçer +- `firebase` FCM / Firebase tabanlı push'u etkinleştirir +- `libre` Firebase bağımlılıkları olmadan derler Kullanışlı Gradle görevleri: ```bash -./gradlew assembleOfficialReleaseTdlibApks -./gradlew assembleTelemtReleaseTdlibApks -./gradlew assembleAllReleaseTdlibApks -./gradlew assembleOfficialDebugTdlibApks -./gradlew assembleTelemtDebugTdlibApks -./gradlew assembleAllDebugTdlibApks +./gradlew :app:assembleOfficialFirebaseRelease +./gradlew :app:assembleTelemtFirebaseRelease +./gradlew :app:assembleOfficialFirebaseDebug +./gradlew :app:assembleTelemtFirebaseDebug +./gradlew :app:assembleOfficialLibreRelease +./gradlew :app:assembleTelemtLibreRelease +./gradlew :app:assembleOfficialLibreDebug +./gradlew :app:assembleTelemtLibreDebug ``` APK adları: -- normal TDLib: `monogram-arm64-v8a--release.apk` -- Telemt TDLib: `monogram-telemt-arm64-v8a--release.apk` +- official Firebase: `monogram-arm64-v8a--release.apk` +- official libre: `monogram-libre-arm64-v8a--release.apk` +- Telemt Firebase: `monogram-telemt-arm64-v8a--release.apk` +- Telemt libre: `monogram-telemt-libre-arm64-v8a--release.apk` --- diff --git a/README_UR.md b/README_UR.md index e80a534c..74fbdfcd 100644 --- a/README_UR.md +++ b/README_UR.md @@ -103,6 +103,9 @@ RELEASE_KEY_PASSWORD=your_key_password ### 3. پش نوٹیفکیشنز کنفیگر کریں +یہ مرحلہ `firebase` build variants کے لیے ضروری ہے۔ اگر آپ صرف `libre` variants بنانا چاہتے ہیں +تو اسے چھوڑ سکتے ہیں۔ + 1. [Firebase console](https://console.firebase.google.com) پر لاگ ان کریں۔ 2. ایک نیا پروجیکٹ بنائیں۔ 3. Firebase میں دو Android apps شامل کریں: @@ -180,26 +183,40 @@ sudo apt-get install build-essential git curl wget php perl gperf unzip zip defa Android Studio میں یہ variants استعمال کریں: -- `officialDebug` -- `officialRelease` -- `telemtDebug` -- `telemtRelease` +- `officialFirebaseDebug` +- `officialFirebaseRelease` +- `officialLibreDebug` +- `officialLibreRelease` +- `telemtFirebaseDebug` +- `telemtFirebaseRelease` +- `telemtLibreDebug` +- `telemtLibreRelease` + +Variant naming: + +- `official` / `telemt` TDLib source منتخب کرتا ہے +- `firebase` FCM / Firebase push کو فعال کرتا ہے +- `libre` Firebase dependencies کے بغیر build کرتا ہے کارآمد Gradle tasks: ```bash -./gradlew assembleOfficialReleaseTdlibApks -./gradlew assembleTelemtReleaseTdlibApks -./gradlew assembleAllReleaseTdlibApks -./gradlew assembleOfficialDebugTdlibApks -./gradlew assembleTelemtDebugTdlibApks -./gradlew assembleAllDebugTdlibApks +./gradlew :app:assembleOfficialFirebaseRelease +./gradlew :app:assembleTelemtFirebaseRelease +./gradlew :app:assembleOfficialFirebaseDebug +./gradlew :app:assembleTelemtFirebaseDebug +./gradlew :app:assembleOfficialLibreRelease +./gradlew :app:assembleTelemtLibreRelease +./gradlew :app:assembleOfficialLibreDebug +./gradlew :app:assembleTelemtLibreDebug ``` APK نام: -- عام TDLib: `monogram-arm64-v8a--release.apk` -- Telemt TDLib: `monogram-telemt-arm64-v8a--release.apk` +- official Firebase: `monogram-arm64-v8a--release.apk` +- official libre: `monogram-libre-arm64-v8a--release.apk` +- Telemt Firebase: `monogram-telemt-arm64-v8a--release.apk` +- Telemt libre: `monogram-telemt-libre-arm64-v8a--release.apk` --- diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cbbcb11f..c11b7780 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,18 +2,28 @@ import com.android.build.api.artifact.SingleArtifact import com.android.build.api.variant.FilterConfiguration import com.android.build.api.variant.impl.VariantOutputImpl import com.google.android.gms.oss.licenses.plugin.DependencyTask -import com.google.gms.googleservices.GoogleServicesPlugin +import org.gradle.api.GradleException import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.google.oss.licenses) - alias(libs.plugins.google.services) alias(libs.plugins.androidx.baselineprofile) } val localProperties = rootProject.extra["localProperties"] as Properties +val googleServicesFile = layout.projectDirectory.file("google-services.json").asFile +val requestedTasks = gradle.startParameter.taskNames +val requestsFirebaseVariant = requestedTasks.any { it.contains("Firebase", ignoreCase = true) } + +if (googleServicesFile.exists()) { + apply(plugin = "com.google.gms.google-services") +} else if (requestsFirebaseVariant) { + throw GradleException( + "Firebase build requested, but app/google-services.json is missing." + ) +} val releaseStoreFile = localProperties.getProperty("RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() } val releaseStorePassword = @@ -50,7 +60,7 @@ android { versionName = "0.1.0" } - flavorDimensions += "tdlib" + flavorDimensions += listOf("tdlib", "runtime") productFlavors { create("official") { @@ -59,6 +69,12 @@ android { create("telemt") { dimension = "tdlib" } + create("firebase") { + dimension = "runtime" + } + create("libre") { + dimension = "runtime" + } } splits { @@ -113,11 +129,16 @@ android { androidComponents { onVariants { variant -> - val flavorName = variant.productFlavors - .map { it.second } - .joinToString("-") - .ifEmpty { "default" } - val apkNamePrefix = if (flavorName == "telemt") "monogram-telemt" else "monogram" + val tdlibFlavor = + variant.productFlavors.firstOrNull { it.first == "tdlib" }?.second ?: "default" + val runtimeFlavor = + variant.productFlavors.firstOrNull { it.first == "runtime" }?.second ?: "default" + val apkNamePrefix = buildString { + append(if (tdlibFlavor == "telemt") "monogram-telemt" else "monogram") + if (runtimeFlavor == "libre") { + append("-libre") + } + } variant.outputs.forEach { output -> val variantOutput = output as? VariantOutputImpl ?: return@forEach @@ -175,9 +196,9 @@ dependencies { implementation(libs.androidx.biometric) implementation(libs.play.services.oss.licenses) - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.messaging) implementation(libs.unifiedpush.connector) + add("firebaseImplementation", platform(libs.firebase.bom)) + add("firebaseImplementation", libs.firebase.messaging) implementation(libs.maplibre.compose) @@ -208,7 +229,3 @@ tasks.withType(DependencyTask::class.java).configureEach { } } } - -googleServices { - missingGoogleServicesStrategy = GoogleServicesPlugin.MissingGoogleServicesStrategy.WARN -} \ No newline at end of file diff --git a/app/src/firebase/java/org/monogram/app/di/FirebaseGmsRuntime.kt b/app/src/firebase/java/org/monogram/app/di/FirebaseGmsRuntime.kt new file mode 100644 index 00000000..a046eefb --- /dev/null +++ b/app/src/firebase/java/org/monogram/app/di/FirebaseGmsRuntime.kt @@ -0,0 +1,17 @@ +package org.monogram.app.di + +import android.content.Context +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.firebase.FirebaseApp + +class FirebaseGmsRuntime( + private val context: Context +) : GmsRuntime { + override val isGmsAvailable: Boolean + get() = GoogleApiAvailability.getInstance() + .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS + + override val isFcmConfigured: Boolean + get() = FirebaseApp.getApps(context).isNotEmpty() +} diff --git a/app/src/firebase/java/org/monogram/app/di/RuntimeOverrideModule.kt b/app/src/firebase/java/org/monogram/app/di/RuntimeOverrideModule.kt new file mode 100644 index 00000000..f5067abf --- /dev/null +++ b/app/src/firebase/java/org/monogram/app/di/RuntimeOverrideModule.kt @@ -0,0 +1,7 @@ +package org.monogram.app.di + +import org.koin.dsl.module + +val runtimeOverrideModule = module { + single { FirebaseGmsRuntime(get()) } +} diff --git a/app/src/libre/java/org/monogram/app/di/LibreGmsRuntime.kt b/app/src/libre/java/org/monogram/app/di/LibreGmsRuntime.kt new file mode 100644 index 00000000..cdeaff68 --- /dev/null +++ b/app/src/libre/java/org/monogram/app/di/LibreGmsRuntime.kt @@ -0,0 +1,6 @@ +package org.monogram.app.di + +class LibreGmsRuntime : GmsRuntime { + override val isGmsAvailable: Boolean = false + override val isFcmConfigured: Boolean = false +} diff --git a/app/src/libre/java/org/monogram/app/di/RuntimeOverrideModule.kt b/app/src/libre/java/org/monogram/app/di/RuntimeOverrideModule.kt new file mode 100644 index 00000000..09e9f9cd --- /dev/null +++ b/app/src/libre/java/org/monogram/app/di/RuntimeOverrideModule.kt @@ -0,0 +1,7 @@ +package org.monogram.app.di + +import org.koin.dsl.module + +val runtimeOverrideModule = module { + single { LibreGmsRuntime() } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7cf541c..d2250ece 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,15 +77,6 @@ android:exported="false" android:process=":crash" android:theme="@style/Theme.MonoGram"/> - - - - - - - { DomainManagerImpl(androidContext(), get().packageName) } factory { AssetsManagerImpl(androidContext()) } - factory { DistrManagerImpl(androidContext()) } + factory { DistrManagerImpl(androidContext(), get()) } factory { ToastMessageDisplayer(androidContext()) } factory { ExternalNavigatorImpl(androidContext()) } factory { DownloadUtils(androidContext(), get()) } diff --git a/app/src/main/java/org/monogram/app/di/DistrManagerImpl.kt b/app/src/main/java/org/monogram/app/di/DistrManagerImpl.kt index 0ea81ceb..2c1f84d1 100644 --- a/app/src/main/java/org/monogram/app/di/DistrManagerImpl.kt +++ b/app/src/main/java/org/monogram/app/di/DistrManagerImpl.kt @@ -2,19 +2,19 @@ package org.monogram.app.di import android.content.Context import android.os.Build -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import com.google.firebase.FirebaseApp import org.monogram.domain.managers.DistrManager import org.unifiedpush.android.connector.UnifiedPush -class DistrManagerImpl(private val context: Context) : DistrManager { +class DistrManagerImpl( + private val context: Context, + private val gmsRuntime: GmsRuntime +) : DistrManager { override fun isGmsAvailable(): Boolean { - return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS + return gmsRuntime.isGmsAvailable } override fun isFcmAvailable(): Boolean { - return FirebaseApp.getApps(context).isNotEmpty() + return gmsRuntime.isFcmConfigured } override fun isUnifiedPushDistributorAvailable(): Boolean { diff --git a/app/src/main/java/org/monogram/app/di/GmsRuntime.kt b/app/src/main/java/org/monogram/app/di/GmsRuntime.kt new file mode 100644 index 00000000..b40a90bc --- /dev/null +++ b/app/src/main/java/org/monogram/app/di/GmsRuntime.kt @@ -0,0 +1,6 @@ +package org.monogram.app.di + +interface GmsRuntime { + val isGmsAvailable: Boolean + val isFcmConfigured: Boolean +} diff --git a/build.gradle.kts b/build.gradle.kts index 531a1f6d..2834a9b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,13 +22,13 @@ extra.set("localProperties", localProperties) tasks.register("assembleOfficialReleaseTdlibApks") { group = "build" description = "Assembles release APKs with the official TDLib prebuilts." - dependsOn(":app:assembleOfficialRelease") + dependsOn(":app:assembleOfficialFirebaseRelease") } tasks.register("assembleTelemtReleaseTdlibApks") { group = "build" description = "Assembles release APKs with the Telemt TDLib prebuilts." - dependsOn(":app:assembleTelemtRelease") + dependsOn(":app:assembleTelemtFirebaseRelease") } tasks.register("assembleAllReleaseTdlibApks") { @@ -43,13 +43,13 @@ tasks.register("assembleAllReleaseTdlibApks") { tasks.register("assembleOfficialDebugTdlibApks") { group = "build" description = "Assembles debug APKs with the official TDLib prebuilts." - dependsOn(":app:assembleOfficialDebug") + dependsOn(":app:assembleOfficialFirebaseDebug") } tasks.register("assembleTelemtDebugTdlibApks") { group = "build" description = "Assembles debug APKs with the Telemt TDLib prebuilts." - dependsOn(":app:assembleTelemtDebug") + dependsOn(":app:assembleTelemtFirebaseDebug") } tasks.register("assembleAllDebugTdlibApks") { diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 5bae7d3c..a95ffbaf 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -27,7 +27,7 @@ android { buildConfigField("boolean", "ENABLE_TDLIB_DEBUG", "false") } - flavorDimensions += "tdlib" + flavorDimensions += listOf("tdlib", "runtime") productFlavors { create("official") { @@ -36,6 +36,12 @@ android { create("telemt") { dimension = "tdlib" } + create("firebase") { + dimension = "runtime" + } + create("libre") { + dimension = "runtime" + } } sourceSets { @@ -82,12 +88,12 @@ dependencies { implementation(project(":domain")) implementation(libs.koin.android) implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.play.services) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.media3.datasource) - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.messaging) implementation(libs.unifiedpush.connector) + add("firebaseImplementation", platform(libs.firebase.bom)) + add("firebaseImplementation", libs.firebase.messaging) + add("firebaseImplementation", libs.kotlinx.coroutines.play.services) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) diff --git a/data/src/firebase/AndroidManifest.xml b/data/src/firebase/AndroidManifest.xml new file mode 100644 index 00000000..b3dce735 --- /dev/null +++ b/data/src/firebase/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/data/src/firebase/java/org/monogram/data/di/FcmRuntimeOverrideModule.kt b/data/src/firebase/java/org/monogram/data/di/FcmRuntimeOverrideModule.kt new file mode 100644 index 00000000..67fc6ee5 --- /dev/null +++ b/data/src/firebase/java/org/monogram/data/di/FcmRuntimeOverrideModule.kt @@ -0,0 +1,9 @@ +package org.monogram.data.di + +import org.koin.dsl.module +import org.monogram.data.push.FcmRuntime +import org.monogram.data.push.FirebaseFcmRuntime + +val fcmRuntimeOverrideModule = module { + single { FirebaseFcmRuntime() } +} diff --git a/data/src/firebase/java/org/monogram/data/push/FirebaseFcmRuntime.kt b/data/src/firebase/java/org/monogram/data/push/FirebaseFcmRuntime.kt new file mode 100644 index 00000000..27980af8 --- /dev/null +++ b/data/src/firebase/java/org/monogram/data/push/FirebaseFcmRuntime.kt @@ -0,0 +1,10 @@ +package org.monogram.data.push + +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await + +class FirebaseFcmRuntime : FcmRuntime { + override val isSupported: Boolean = true + + override suspend fun fetchToken(): String? = FirebaseMessaging.getInstance().token.await() +} diff --git a/data/src/firebase/java/org/monogram/data/service/FcmPushService.kt b/data/src/firebase/java/org/monogram/data/service/FcmPushService.kt new file mode 100644 index 00000000..509fd942 --- /dev/null +++ b/data/src/firebase/java/org/monogram/data/service/FcmPushService.kt @@ -0,0 +1,34 @@ +package org.monogram.data.service + +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import org.koin.android.ext.android.inject +import org.monogram.data.gateway.TelegramGateway +import org.monogram.domain.repository.AppPreferencesProvider + +class FcmPushService : FirebaseMessagingService() { + private val gateway: TelegramGateway by inject() + private val appPreferences: AppPreferencesProvider by inject() + private val delegate by lazy { + BaseFcmPushService( + context = this, + gateway = gateway, + appPreferences = appPreferences + ) + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + delegate.handleNewToken(token) + } + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + delegate.handleMessage(message.data) + } + + override fun onDeletedMessages() { + super.onDeletedMessages() + delegate.handleDeletedMessages() + } +} diff --git a/data/src/libre/java/org/monogram/data/di/FcmRuntimeOverrideModule.kt b/data/src/libre/java/org/monogram/data/di/FcmRuntimeOverrideModule.kt new file mode 100644 index 00000000..64ea43a9 --- /dev/null +++ b/data/src/libre/java/org/monogram/data/di/FcmRuntimeOverrideModule.kt @@ -0,0 +1,9 @@ +package org.monogram.data.di + +import org.koin.dsl.module +import org.monogram.data.push.FcmRuntime +import org.monogram.data.push.NoOpFcmRuntime + +val fcmRuntimeOverrideModule = module { + single { NoOpFcmRuntime() } +} diff --git a/data/src/libre/java/org/monogram/data/push/NoOpFcmRuntime.kt b/data/src/libre/java/org/monogram/data/push/NoOpFcmRuntime.kt new file mode 100644 index 00000000..c793b9da --- /dev/null +++ b/data/src/libre/java/org/monogram/data/push/NoOpFcmRuntime.kt @@ -0,0 +1,7 @@ +package org.monogram.data.push + +class NoOpFcmRuntime : FcmRuntime { + override val isSupported: Boolean = false + + override suspend fun fetchToken(): String? = null +} diff --git a/data/src/main/java/org/monogram/data/di/TdNotificationManager.kt b/data/src/main/java/org/monogram/data/di/TdNotificationManager.kt index adbb0a22..43b494c6 100644 --- a/data/src/main/java/org/monogram/data/di/TdNotificationManager.kt +++ b/data/src/main/java/org/monogram/data/di/TdNotificationManager.kt @@ -28,13 +28,11 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.graphics.createBitmap -import com.google.firebase.messaging.FirebaseMessaging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withTimeoutOrNull import org.drinkless.tdlib.TdApi import org.monogram.data.core.coRunCatching @@ -45,6 +43,7 @@ import org.monogram.data.infra.FileDownloadQueue import org.monogram.data.notifications.NotificationMuteDecision import org.monogram.data.notifications.NotificationMuteResolver import org.monogram.data.notifications.NotificationScopeState +import org.monogram.data.push.FcmRuntime import org.monogram.data.push.UnifiedPushManager import org.monogram.data.service.NotificationDismissReceiver import org.monogram.data.service.NotificationReadReceiver @@ -54,8 +53,8 @@ import org.monogram.domain.repository.NotificationSettingsRepository import org.monogram.domain.repository.NotificationSettingsRepository.TdNotificationScope import org.monogram.domain.repository.PushProvider import org.monogram.domain.repository.StringProvider -import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.min class TdNotificationManager( @@ -66,6 +65,7 @@ class TdNotificationManager( private val notificationSettingDao: NotificationSettingDao, private val fileQueue: FileDownloadQueue, private val stringProvider: StringProvider, + private val fcmRuntime: FcmRuntime, private val unifiedPushManager: UnifiedPushManager, private val muteResolver: NotificationMuteResolver ) { @@ -264,8 +264,16 @@ class TdNotificationManager( when (appPreferences.pushProvider.value) { PushProvider.FCM -> { coRunCatching { + if (!fcmRuntime.isSupported) { + Log.w(TAG, "FCM runtime is not available in this build") + return@coRunCatching + } unifiedPushManager.unregister() - val token = FirebaseMessaging.getInstance().token.await() + val token = fcmRuntime.fetchToken() + if (token.isNullOrBlank()) { + Log.w(TAG, "FCM token is not available") + return@coRunCatching + } gateway.execute( TdApi.RegisterDevice( TdApi.DeviceTokenFirebaseCloudMessaging(token, true), diff --git a/data/src/main/java/org/monogram/data/di/dataModule.kt b/data/src/main/java/org/monogram/data/di/dataModule.kt index 598e01e4..a1020704 100644 --- a/data/src/main/java/org/monogram/data/di/dataModule.kt +++ b/data/src/main/java/org/monogram/data/di/dataModule.kt @@ -160,6 +160,8 @@ import org.monogram.domain.repository.WallpaperRepository import org.monogram.domain.repository.WebAppRepository val dataModule = module { + includes(fcmRuntimeOverrideModule) + single { CoroutineScope(SupervisorJob() + get().default) } single(createdAtStart = true) { TdLibClient() } @@ -839,6 +841,7 @@ val dataModule = module { get(), get(), get(), + get(), get() ) } diff --git a/data/src/main/java/org/monogram/data/push/FcmRuntime.kt b/data/src/main/java/org/monogram/data/push/FcmRuntime.kt new file mode 100644 index 00000000..9641e29c --- /dev/null +++ b/data/src/main/java/org/monogram/data/push/FcmRuntime.kt @@ -0,0 +1,7 @@ +package org.monogram.data.push + +interface FcmRuntime { + val isSupported: Boolean + + suspend fun fetchToken(): String? +} diff --git a/data/src/main/java/org/monogram/data/service/FcmPushService.kt b/data/src/main/java/org/monogram/data/service/BaseFcmPushService.kt similarity index 61% rename from data/src/main/java/org/monogram/data/service/FcmPushService.kt rename to data/src/main/java/org/monogram/data/service/BaseFcmPushService.kt index 272d3f76..ba8e8ac1 100644 --- a/data/src/main/java/org/monogram/data/service/FcmPushService.kt +++ b/data/src/main/java/org/monogram/data/service/BaseFcmPushService.kt @@ -1,9 +1,8 @@ package org.monogram.data.service +import android.content.Context import android.os.PowerManager import android.util.Log -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -12,19 +11,19 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.drinkless.tdlib.TdApi import org.json.JSONObject -import org.koin.android.ext.android.inject import org.monogram.data.gateway.TelegramGateway import org.monogram.domain.repository.AppPreferencesProvider import org.monogram.domain.repository.PushProvider -class FcmPushService : FirebaseMessagingService() { - private val gateway: TelegramGateway by inject() - private val appPreferences: AppPreferencesProvider by inject() +class BaseFcmPushService( + private val context: Context, + private val gateway: TelegramGateway, + private val appPreferences: AppPreferencesProvider +) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - override fun onNewToken(token: String) { - super.onNewToken(token) - Log.d("FcmPushService", "New FCM token: $token") + fun handleNewToken(token: String) { + Log.d(TAG, "New FCM token: $token") if (appPreferences.pushProvider.value == PushProvider.FCM) { scope.launch { registerToken(token) @@ -32,26 +31,22 @@ class FcmPushService : FirebaseMessagingService() { } } - override fun onMessageReceived(message: RemoteMessage) { - super.onMessageReceived(message) - Log.d("FcmPushService", "FCM message received: ${message.data}") + fun handleMessage(data: Map) { + Log.d(TAG, "FCM message received: $data") if (appPreferences.pushProvider.value != PushProvider.FCM) return - - val data = message.data if (data.isEmpty()) return - val powerManager = getSystemService(POWER_SERVICE) as? PowerManager ?: return + val powerManager = + context.getSystemService(Context.POWER_SERVICE) as? PowerManager ?: return val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "monogram:FcmPushService") - .apply { - setReferenceCounted(false) - } + .apply { setReferenceCounted(false) } try { val json = JSONObject() - for ((k, v) in data) { - json.put(k, v) + for ((key, value) in data) { + json.put(key, value) } val jsonPayload = json.toString() if (jsonPayload.isBlank()) return @@ -62,10 +57,10 @@ class FcmPushService : FirebaseMessagingService() { withTimeout(8_000L) { gateway.execute(TdApi.ProcessPushNotification(jsonPayload)) } - Log.d("FcmPushService", "ProcessPushNotification success") + Log.d(TAG, "ProcessPushNotification success") } catch (e: Exception) { if (e is CancellationException) throw e - Log.e("FcmPushService", "Error processing push", e) + Log.e(TAG, "Error processing push", e) } finally { if (wakeLock.isHeld) { wakeLock.release() @@ -73,16 +68,15 @@ class FcmPushService : FirebaseMessagingService() { } } } catch (e: Exception) { - Log.e("FcmPushService", "Error preparing push payload", e) + Log.e(TAG, "Error preparing push payload", e) if (wakeLock.isHeld) { wakeLock.release() } } } - override fun onDeletedMessages() { - super.onDeletedMessages() - Log.d("FcmPushService", "FCM messages deleted") + fun handleDeletedMessages() { + Log.d(TAG, "FCM messages deleted") } private suspend fun registerToken(token: String) { @@ -95,10 +89,14 @@ class FcmPushService : FirebaseMessagingService() { longArrayOf() ) ) - Log.d("FcmPushService", "RegisterDevice result: $result") + Log.d(TAG, "RegisterDevice result: $result") } catch (e: Exception) { if (e is CancellationException) throw e - Log.e("FcmPushService", "RegisterDevice failed", e) + Log.e(TAG, "RegisterDevice failed", e) } } -} \ No newline at end of file + + private companion object { + const val TAG = "FcmPushService" + } +} diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index c835ba07..cba4fd06 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -22,7 +22,7 @@ android { } } - flavorDimensions += "tdlib" + flavorDimensions += listOf("tdlib", "runtime") productFlavors { create("official") { @@ -33,6 +33,12 @@ android { dimension = "tdlib" buildConfigField("boolean", "ENABLE_TELEMT_DNS", "true") } + create("firebase") { + dimension = "runtime" + } + create("libre") { + dimension = "runtime" + } } buildTypes { @@ -82,13 +88,11 @@ dependencies { implementation(libs.bundles.koin) implementation(libs.kotlinx.serialization.json) - implementation(libs.play.services.mlkit.barcode.scanning) implementation(libs.zxing.core) implementation(libs.androidx.biometric) implementation(libs.androidx.security.crypto) implementation(libs.maplibre.compose) implementation(libs.play.services.oss.licenses) - implementation(libs.play.services.location) implementation(libs.unifiedpush.connector) implementation(libs.androidx.compose.ui.tooling.preview) diff --git a/presentation/src/main/java/org/monogram/presentation/core/ui/IntegratedQRScanner.kt b/presentation/src/main/java/org/monogram/presentation/core/ui/IntegratedQRScanner.kt index efe57a19..f3a3e925 100644 --- a/presentation/src/main/java/org/monogram/presentation/core/ui/IntegratedQRScanner.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/ui/IntegratedQRScanner.kt @@ -1,8 +1,9 @@ package org.monogram.presentation.core.ui +import android.annotation.SuppressLint import androidx.camera.core.CameraSelector -import androidx.camera.mlkit.vision.MlKitAnalyzer -import androidx.camera.view.CameraController +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.compose.foundation.layout.Box @@ -15,7 +16,12 @@ import androidx.compose.material.icons.rounded.FlashOff import androidx.compose.material.icons.rounded.FlashOn import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -25,10 +31,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat -import com.google.mlkit.vision.barcode.BarcodeScannerOptions -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.common.Barcode +import com.google.zxing.BinaryBitmap +import com.google.zxing.MultiFormatReader +import com.google.zxing.NotFoundException +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeReader import org.monogram.presentation.R +import java.nio.ByteBuffer @Composable fun IntegratedQRScanner( @@ -47,23 +57,12 @@ fun IntegratedQRScanner( var torchEnabled by remember { mutableStateOf(false) } LaunchedEffect(Unit) { - val barcodeScanner = BarcodeScanning.getClient( - BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() - ) val executor = ContextCompat.getMainExecutor(context) cameraController.setImageAnalysisAnalyzer( executor, - MlKitAnalyzer( - listOf(barcodeScanner), - CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED, - executor - ) { result -> - val barcode = result.getValue(barcodeScanner)?.firstOrNull() - val code = barcode?.rawValue - if (!code.isNullOrEmpty() && code != lastScannedCode) { + ZxingQrAnalyzer { code -> + if (code != lastScannedCode) { lastScannedCode = code onCodeDetected(code) } @@ -115,3 +114,56 @@ fun IntegratedQRScanner( } } } + +private class ZxingQrAnalyzer( + private val onCodeDetected: (String) -> Unit +) : ImageAnalysis.Analyzer { + private val reader = QRCodeReader() + private val fallbackReader = MultiFormatReader() + + @SuppressLint("UnsafeOptInUsageError") + override fun analyze(image: ImageProxy) { + val plane = image.planes.firstOrNull() + if (plane == null) { + image.close() + return + } + + val bytes = plane.buffer.toByteArray() + val source = PlanarYUVLuminanceSource( + bytes, + image.width, + image.height, + 0, + 0, + image.width, + image.height, + false + ) + + val bitmap = BinaryBitmap(HybridBinarizer(source)) + val text = decode(bitmap) + if (!text.isNullOrBlank()) { + onCodeDetected(text) + } + image.close() + } + + private fun decode(bitmap: BinaryBitmap): String? { + return try { + reader.decode(bitmap).text + } catch (_: NotFoundException) { + runCatching { fallbackReader.decode(bitmap).text }.getOrNull() + } catch (_: Exception) { + null + } finally { + reader.reset() + fallbackReader.reset() + } + } +} + +private fun ByteBuffer.toByteArray(): ByteArray { + rewind() + return ByteArray(remaining()).also { get(it) } +} diff --git a/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt b/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt index 5f244add..283c7330 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt @@ -3,22 +3,87 @@ package org.monogram.presentation.settings.profile import android.Manifest +import android.annotation.SuppressLint +import android.content.Context import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.os.Looper import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.AlternateEmail +import androidx.compose.material.icons.rounded.Business +import androidx.compose.material.icons.rounded.Cake +import androidx.compose.material.icons.rounded.CameraAlt +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.Link +import androidx.compose.material.icons.rounded.LocationOn +import androidx.compose.material.icons.rounded.Map +import androidx.compose.material.icons.rounded.MyLocation +import androidx.compose.material.icons.rounded.Person +import androidx.compose.material.icons.rounded.PersonOutline +import androidx.compose.material.icons.rounded.Schedule +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ContainedLoadingIndicator +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -33,14 +98,14 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.Priority import com.maplibre.compose.MapView import com.maplibre.compose.camera.CameraState import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.rememberSaveableMapViewCamera import com.maplibre.compose.symbols.Symbol import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapLibreMapOptions import org.monogram.domain.models.BirthdateModel @@ -49,10 +114,13 @@ import org.monogram.domain.models.BusinessOpeningHoursModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.core.util.FileUtils import org.monogram.presentation.core.ui.SectionHeader import org.monogram.presentation.core.ui.SettingsTextField -import java.util.* +import org.monogram.presentation.core.util.FileUtils +import java.util.Calendar +import java.util.Collections +import java.util.TimeZone +import kotlin.coroutines.resume private const val MAP_STYLE = "https://tiles.openfreemap.org/styles/bright" @@ -61,6 +129,7 @@ private const val MAP_STYLE = "https://tiles.openfreemap.org/styles/bright" fun EditProfileContent(component: EditProfileComponent) { val state by component.state.subscribeAsState() val context = LocalContext.current + val scope = rememberCoroutineScope() val photoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), @@ -474,7 +543,8 @@ fun EditProfileContent(component: EditProfileComponent) { .clip(CircleShape) .background(if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant) .clickable { - selectedDays = if (isSelected) selectedDays - dayNum else selectedDays + dayNum + selectedDays = + if (isSelected) selectedDays - dayNum else selectedDays + dayNum }, contentAlignment = Alignment.Center ) { @@ -590,21 +660,19 @@ fun EditProfileContent(component: EditProfileComponent) { ) ) - val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true || permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true ) { - fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null) - .addOnSuccessListener { location -> - location?.let { - selectedLatitude = it.latitude - selectedLongitude = it.longitude - component.onReverseGeocode(it.latitude, it.longitude) - } + scope.launch { + context.getCurrentLocationCompat()?.let { + selectedLatitude = it.latitude + selectedLongitude = it.longitude + component.onReverseGeocode(it.latitude, it.longitude) } + } } } @@ -627,19 +695,23 @@ fun EditProfileContent(component: EditProfileComponent) { }, actions = { IconButton(onClick = { - if (ContextCompat.checkSelfPermission( + if ( + ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { - fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null) - .addOnSuccessListener { location -> - location?.let { - selectedLatitude = it.latitude - selectedLongitude = it.longitude - component.onReverseGeocode(it.latitude, it.longitude) - } + scope.launch { + context.getCurrentLocationCompat()?.let { + selectedLatitude = it.latitude + selectedLongitude = it.longitude + component.onReverseGeocode(it.latitude, it.longitude) } + } } else { permissionLauncher.launch( arrayOf( @@ -1091,3 +1163,85 @@ fun EditProfileContent(component: EditProfileComponent) { } } } + +@SuppressLint("MissingPermission") +private suspend fun Context.getCurrentLocationCompat(): Location? = + suspendCancellableCoroutine { cont -> + val locationManager = getSystemService(Context.LOCATION_SERVICE) as? LocationManager + if (locationManager == null) { + cont.resume(null) + return@suspendCancellableCoroutine + } + + val hasFine = ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + val hasCoarse = ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + if (!hasFine && !hasCoarse) { + cont.resume(null) + return@suspendCancellableCoroutine + } + + val providers = locationManager.getProviders(true) + var bestLocation: Location? = null + for (provider in providers) { + if (provider == LocationManager.GPS_PROVIDER && !hasFine) continue + if (provider == LocationManager.NETWORK_PROVIDER && !hasFine && !hasCoarse) continue + + val location = + runCatching { locationManager.getLastKnownLocation(provider) }.getOrNull() + if (location != null && (bestLocation == null || location.time > bestLocation.time)) { + bestLocation = location + } + } + + if (bestLocation != null && System.currentTimeMillis() - bestLocation.time < 60_000L) { + cont.resume(bestLocation) + return@suspendCancellableCoroutine + } + + val provider = when { + hasFine && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) -> LocationManager.GPS_PROVIDER + (hasFine || hasCoarse) && locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> LocationManager.NETWORK_PROVIDER + else -> null + } + + if (provider == null) { + cont.resume(bestLocation) + return@suspendCancellableCoroutine + } + + val listener = object : LocationListener { + override fun onLocationChanged(location: Location) { + if (cont.isActive) { + cont.resume(location) + } + locationManager.removeUpdates(this) + } + + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit + override fun onProviderEnabled(provider: String) = Unit + override fun onProviderDisabled(provider: String) { + if (cont.isActive) { + cont.resume(bestLocation) + } + } + } + + runCatching { + locationManager.requestSingleUpdate(provider, listener, Looper.getMainLooper()) + }.onFailure { + if (cont.isActive) { + cont.resume(bestLocation) + } + } + + cont.invokeOnCancellation { + locationManager.removeUpdates(listener) + } + }