diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 0d770749e4..e70a474f25 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -205,6 +205,10 @@ jobs: with: name: android path: packages/frontend/apps/android/dist + - name: Load Google Service file + env: + DATA: ${{ secrets.FIREBASE_ANDROID_GOOGLE_SERVICE_JSON }} + run: echo $DATA | base64 -di > packages/frontend/apps/android/App/app/google-services.json - name: Setup Node.js uses: ./.github/actions/setup-node timeout-minutes: 10 diff --git a/packages/frontend/apps/android/App/app/.gitignore b/packages/frontend/apps/android/App/app/.gitignore index 043df802a2..5fc4796e46 100644 --- a/packages/frontend/apps/android/App/app/.gitignore +++ b/packages/frontend/apps/android/App/app/.gitignore @@ -1,2 +1,3 @@ /build/* !/build/.npmkeep +google-services.json diff --git a/packages/frontend/apps/android/App/app/build.gradle b/packages/frontend/apps/android/App/app/build.gradle index c53a0c838c..336be8d87d 100644 --- a/packages/frontend/apps/android/App/app/build.gradle +++ b/packages/frontend/apps/android/App/app/build.gradle @@ -3,13 +3,15 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { alias libs.plugins.android.application - alias libs.plugins.compose - alias libs.plugins.hilt alias libs.plugins.kotlin.android alias libs.plugins.kotlin.parcelize alias libs.plugins.kotlin.serialization alias libs.plugins.ksp alias libs.plugins.rust.android + alias libs.plugins.compose + alias libs.plugins.google.service + alias libs.plugins.firebase.crashlytics + alias libs.plugins.hilt } apply from: 'capacitor.build.gradle' @@ -106,6 +108,7 @@ dependencies { implementation libs.androidx.coordinatorlayout implementation libs.androidx.core.ktx implementation libs.androidx.core.splashscreen + implementation libs.androidx.datastore.preferences implementation libs.androidx.navigation.fragment implementation libs.androidx.navigation.ui.ktx @@ -118,8 +121,11 @@ dependencies { implementation libs.kotlinx.coroutines.android implementation libs.kotlinx.serialization.json - def okhttpBom = platform(libs.okhttp.bom) - implementation okhttpBom + implementation platform(libs.firebase.bom) + implementation libs.firebase.analytics + implementation libs.firebase.crashlytics + + implementation platform(libs.okhttp.bom) implementation libs.okhttp implementation libs.okhttp.coroutines implementation libs.okhttp.logging @@ -130,15 +136,6 @@ dependencies { androidTestImplementation libs.androidx.espresso.core } -try { - def servicesJSON = file('google-services.json') - if (servicesJSON.text) { - apply plugin: 'com.google.gms.google-services' - } -} catch (Exception ignored) { - logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") -} - cargo { module = "../../../../mobile-native" libname = "affine_mobile_native" diff --git a/packages/frontend/apps/android/App/app/proguard-rules.pro b/packages/frontend/apps/android/App/app/proguard-rules.pro index de90aa873c..d514dc988c 100644 --- a/packages/frontend/apps/android/App/app/proguard-rules.pro +++ b/packages/frontend/apps/android/App/app/proguard-rules.pro @@ -68,3 +68,8 @@ -keepclassmembers public class **$$serializer { private ** descriptor; } + +# Keep file names and line numbers. +-keepattributes SourceFile,LineNumberTable +# Keep custom exceptions. +-keep public class * extends java.lang.Exception diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/AffineApp.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/AffineApp.kt index 359c5383a6..9961908d64 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/AffineApp.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/AffineApp.kt @@ -1,7 +1,22 @@ package app.affine.pro +import android.annotation.SuppressLint import android.app.Application +import android.content.Context +import app.affine.pro.service.CookieStore +import app.affine.pro.utils.dataStore +import app.affine.pro.utils.get +import app.affine.pro.utils.logger.AffineDebugTree +import app.affine.pro.utils.logger.CrashlyticsTree +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.crashlytics.setCustomKeys +import com.google.firebase.ktx.Firebase import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import okhttp3.Cookie +import okhttp3.HttpUrl.Companion.toHttpUrl import timber.log.Timber @HiltAndroidApp @@ -9,8 +24,47 @@ class AffineApp : Application() { override fun onCreate() { super.onCreate() - if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + _context = applicationContext + // init logger + Timber.plant(if (BuildConfig.DEBUG) AffineDebugTree() else CrashlyticsTree()) + // init capacitor config CapacitorConfig.init(baseContext) + // init crashlytics + Firebase.crashlytics.setCustomKeys { + key("affine_version", CapacitorConfig.getAffineVersion()) + } + // init cookies from local + MainScope().launch(Dispatchers.IO) { + val sessionCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_SESSION) + val userIdCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_USER_ID) + if (sessionCookieStr.isEmpty() || userIdCookieStr.isEmpty()) { + Timber.i("[init] user has not signed in yet.") + return@launch + } + Timber.i("[init] user already signed in.") + try { + val cookies = listOf( + Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), sessionCookieStr) + ?: error("Parse session cookie fail:[ cookie = $sessionCookieStr ]"), + Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), userIdCookieStr) + ?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"), + ) + CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies) + } catch (e: Exception) { + Timber.w(e, "[init] load persistent cookies fail.") + } + } } + override fun onTerminate() { + _context = null + super.onTerminate() + } + + companion object { + @SuppressLint("StaticFieldLeak") + private var _context: Context? = null + + fun context() = requireNotNull(_context) + } } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/Constants.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt similarity index 96% rename from packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/Constants.kt rename to packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt index eaf8268772..046d909327 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/Constants.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/Constants.kt @@ -1,4 +1,4 @@ -package app.affine.pro.ai +package app.affine.pro enum class Prompt(val value: String) { Summary("Summary"), diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt index d43eb389c1..cca5cb07b9 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/ai/chat/ChatViewModel.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.coroutineContext @HiltViewModel class ChatViewModel @Inject constructor( @@ -37,10 +36,10 @@ class ChatViewModel @Inject constructor( workspaceId = webRepo.workspaceId(), docId = webRepo.docId(), ).getOrElse { - Timber.d("Create session failed") + Timber.w(it, "Create session failed") return@launch } - Timber.d("Create session: $sessionId") + Timber.i("Create session success:[ sessionId = $sessionId].") val historyMessages = graphQLRepo.getCopilotHistories( workspaceId = webRepo.workspaceId(), docId = webRepo.docId(), @@ -60,16 +59,12 @@ class ChatViewModel @Inject constructor( sessionId = sessionId, message = message, ).onSuccess { messageId -> - Timber.d("send message: $messageId") + Timber.i("send message: $messageId") sseRepo.messageStream(sessionId, messageId) .onEach { - Timber.d("$coroutineContext") - Timber.d("on message: ${it.getOrNull()}") + Timber.d("On sse message: ${it.getOrNull()}") } .flowOn(Dispatchers.IO) - .onEach { - Timber.d("$coroutineContext") - } .collect() } } @@ -80,7 +75,7 @@ class ChatViewModel @Inject constructor( docId = webRepo.docId(), ).onSuccess { id -> sessionId = id - Timber.d("Create session: $id") + Timber.i("Create session: $id") sendMessage() }.onFailure { Timber.e(it, "Create session failed.") diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AIButtonPlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AIButtonPlugin.kt index a692d643b9..18eb9ed8b7 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AIButtonPlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AIButtonPlugin.kt @@ -4,6 +4,7 @@ import com.getcapacitor.Plugin import com.getcapacitor.PluginCall import com.getcapacitor.PluginMethod import com.getcapacitor.annotation.CapacitorPlugin +import timber.log.Timber @CapacitorPlugin(name = "AIButton") class AIButtonPlugin : Plugin() { @@ -16,6 +17,7 @@ class AIButtonPlugin : Plugin() { @PluginMethod fun present(call: PluginCall) { launch { + Timber.i("present AIButton") (activity as? Callback)?.present() call.resolve() } @@ -24,6 +26,7 @@ class AIButtonPlugin : Plugin() { @PluginMethod fun dismiss(call: PluginCall) { launch { + Timber.i("dismiss AIButton") (activity as? Callback)?.dismiss() call.resolve() } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt index 2ffe44663c..25d3ffbe6b 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AffineThemePlugin.kt @@ -4,6 +4,7 @@ import com.getcapacitor.Plugin import com.getcapacitor.PluginCall import com.getcapacitor.PluginMethod import com.getcapacitor.annotation.CapacitorPlugin +import timber.log.Timber @CapacitorPlugin(name = "AffineTheme") class AffineThemePlugin : Plugin() { @@ -14,7 +15,9 @@ class AffineThemePlugin : Plugin() { @PluginMethod fun onThemeChanged(call: PluginCall) { - (bridge.activity as? Callback)?.onThemeChanged(call.data.optBoolean("darkMode")) + val darkMode = call.data.optBoolean("darkMode") + Timber.i("onThemeChanged:[ darkMode = $darkMode ]") + (bridge.activity as? Callback)?.onThemeChanged(darkMode) call.resolve() } } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt index 1d762d32f8..040c8f6ef4 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/AuthPlugin.kt @@ -1,9 +1,12 @@ package app.affine.pro.plugin import android.annotation.SuppressLint +import app.affine.pro.AffineApp import app.affine.pro.CapacitorConfig import app.affine.pro.service.CookieStore import app.affine.pro.service.OkHttp +import app.affine.pro.utils.clear +import app.affine.pro.utils.dataStore import com.getcapacitor.JSObject import com.getcapacitor.Plugin import com.getcapacitor.PluginCall @@ -16,6 +19,7 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.coroutines.executeAsync import org.json.JSONObject +import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) @CapacitorPlugin(name = "Auth") @@ -23,120 +27,18 @@ class AuthPlugin : Plugin() { @PluginMethod fun signInMagicLink(call: PluginCall) { - launch(Dispatchers.IO) { - try { - val endpoint = call.getStringEnsure("endpoint") - val email = call.getStringEnsure("email") - val token = call.getStringEnsure("token") - val clientNonce = call.getString("clientNonce") - val body = JSONObject() - .apply { - put("email", email) - put("token", token) - put("client_nonce", clientNonce) - } - .toString() - .toRequestBody("application/json".toMediaTypeOrNull()) - - val request = Request.Builder() - .url("$endpoint/api/auth/magic-link") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) - .post(body) - .build() - OkHttp.client.newCall(request).executeAsync().use { response -> - if (response.code >= 400) { - call.reject(response.body.string()) - return@launch - } - CookieStore.getCookie(endpoint, "affine_session")?.let { - call.resolve(JSObject().put("token", it)) - } ?: call.reject("token not found") - } - } catch (e: Exception) { - call.reject("Failed to sign in, $e", null, e) - } - } + processSignIn(call, SignInMethod.MagicLink) } @PluginMethod fun signInOauth(call: PluginCall) { - launch(Dispatchers.IO) { - try { - val endpoint = call.getStringEnsure("endpoint") - val code = call.getStringEnsure("code") - val state = call.getStringEnsure("state") - val clientNonce = call.getString("clientNonce") - val body = JSONObject() - .apply { - put("code", code) - put("state", state) - put("client_nonce", clientNonce) - } - .toString() - .toRequestBody("application/json".toMediaTypeOrNull()) - - val request = Request.Builder() - .url("$endpoint/api/oauth/callback") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) - .post(body) - .build() - OkHttp.client.newCall(request).executeAsync().use { response -> - if (response.code >= 400) { - call.reject(response.body.string()) - return@launch - } - CookieStore.getCookie(endpoint, "affine_session")?.let { - call.resolve(JSObject().put("token", it)) - } ?: call.reject("token not found") - } - } catch (e: Exception) { - call.reject("Failed to sign in, $e", null, e) - } - } + processSignIn(call, SignInMethod.Oauth) } @SuppressLint("BuildListAdds") @PluginMethod fun signInPassword(call: PluginCall) { - launch(Dispatchers.IO) { - try { - val endpoint = call.getStringEnsure("endpoint") - val email = call.getStringEnsure("email") - val password = call.getStringEnsure("password") - val verifyToken = call.getString("verifyToken") - val challenge = call.getString("challenge") - val body = JSONObject() - .apply { - put("email", email) - put("password", password) - } - .toString() - .toRequestBody("application/json".toMediaTypeOrNull()) - - - val requestBuilder = Request.Builder() - .url("$endpoint/api/auth/sign-in") - .header("x-affine-version", CapacitorConfig.getAffineVersion()) - .post(body) - if (verifyToken != null) { - requestBuilder.addHeader("x-captcha-token", verifyToken) - } - if (challenge != null) { - requestBuilder.addHeader("x-captcha-challenge", challenge) - } - OkHttp.client.newCall(requestBuilder.build()).executeAsync().use { response -> - if (response.code >= 400) { - call.reject(response.body.string()) - return@launch - } - CookieStore.getCookie(endpoint, "affine_session")?.let { - call.resolve(JSObject().put("token", it)) - } ?: call.reject("token not found") - } - } catch (e: Exception) { - call.reject("Failed to sign in, $e", null, e) - } - } + processSignIn(call, SignInMethod.Password) } @PluginMethod @@ -154,11 +56,110 @@ class AuthPlugin : Plugin() { call.reject(response.body.string()) return@launch } + Timber.i("Sign out success.") call.resolve(JSObject().put("ok", true)) } } catch (e: Exception) { + Timber.w(e, "Sign out fail.") call.reject("Failed to sign out, $e", null, e) } } } + + private enum class SignInMethod { + Password, Oauth, MagicLink + } + + private fun processSignIn(call: PluginCall, method: SignInMethod) { + launch(Dispatchers.IO) { + try { + val endpoint = call.getStringEnsure("endpoint") + val request = when (method) { + SignInMethod.Password -> { + val email = call.getStringEnsure("email") + val password = call.getStringEnsure("password") + val verifyToken = call.getString("verifyToken") + val challenge = call.getString("challenge") + val body = JSONObject() + .apply { + put("email", email) + put("password", password) + } + .toString() + .toRequestBody("application/json".toMediaTypeOrNull()) + + val requestBuilder = Request.Builder() + .url("$endpoint/api/auth/sign-in") + .header("x-affine-version", CapacitorConfig.getAffineVersion()) + .post(body) + if (verifyToken != null) { + requestBuilder.addHeader("x-captcha-token", verifyToken) + } + if (challenge != null) { + requestBuilder.addHeader("x-captcha-challenge", challenge) + } + requestBuilder.build() + } + + SignInMethod.Oauth -> { + val code = call.getStringEnsure("code") + val state = call.getStringEnsure("state") + val clientNonce = call.getString("clientNonce") + val body = JSONObject() + .apply { + put("code", code) + put("state", state) + put("client_nonce", clientNonce) + } + .toString() + .toRequestBody("application/json".toMediaTypeOrNull()) + + Request.Builder() + .url("$endpoint/api/oauth/callback") + .header("x-affine-version", CapacitorConfig.getAffineVersion()) + .post(body) + .build() + } + + SignInMethod.MagicLink -> { + val email = call.getStringEnsure("email") + val token = call.getStringEnsure("token") + val clientNonce = call.getString("clientNonce") + val body = JSONObject() + .apply { + put("email", email) + put("token", token) + put("client_nonce", clientNonce) + } + .toString() + .toRequestBody("application/json".toMediaTypeOrNull()) + + Request.Builder() + .url("$endpoint/api/auth/magic-link") + .header("x-affine-version", CapacitorConfig.getAffineVersion()) + .post(body) + .build() + } + } + + OkHttp.client.newCall(request).executeAsync().use { response -> + if (response.code >= 400) { + call.reject(response.body.string()) + return@launch + } + CookieStore.getCookie(endpoint, CookieStore.AFFINE_SESSION)?.let { + Timber.i("$method sign in success.") + Timber.d("Update session [$it]") + call.resolve(JSObject().put("token", it)) + } ?: run { + Timber.w("$method sign in fail, token not found.") + call.reject("$method sign in fail, token not found") + } + } + } catch (e: Exception) { + Timber.w(e, "$method sign in fail.") + call.reject("$method sign in fail.", null, e) + } + } + } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/HashCashPlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/HashCashPlugin.kt index 32994601ba..4ddc0d64bd 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/HashCashPlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/HashCashPlugin.kt @@ -6,6 +6,7 @@ import com.getcapacitor.PluginCall import com.getcapacitor.PluginMethod import com.getcapacitor.annotation.CapacitorPlugin import kotlinx.coroutines.Dispatchers +import timber.log.Timber import uniffi.affine_mobile_native.hashcashMint @CapacitorPlugin(name = "HashCash") @@ -16,8 +17,10 @@ class HashCashPlugin : Plugin() { launch(Dispatchers.IO) { val challenge = call.getString("challenge") ?: "" val bits = call.getInt("bits") ?: 20 + val hash = hashcashMint(resource = challenge, bits = bits.toUInt()) + Timber.i("hash:[ value = $hash ]") call.resolve(JSObject().apply { - put("value", hashcashMint(resource = challenge, bits = bits.toUInt())) + put("value", hash) }) } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/NbStorePlugin.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/NbStorePlugin.kt index 7fcbc45a0b..f5847ff404 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/NbStorePlugin.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/plugin/NbStorePlugin.kt @@ -28,7 +28,8 @@ class NbStorePlugin : Plugin() { val spaceType = call.getStringEnsure("spaceType") val peer = call.getStringEnsure("peer") val appStoragePath = activity?.filesDir ?: run { - call.reject("Failed to connect storage, cannot access file system.") + Timber.w("Failed to connect storage, cannot access device file system.") + call.reject("Failed to connect storage, cannot access device file system.") return@launch } val peerDir = appStoragePath.resolve("workspaces") @@ -38,13 +39,15 @@ class NbStorePlugin : Plugin() { .replace(Regex("_+"), "_") .replace(Regex("_+$"), "") ) - Timber.d("connecting nbstore... peerDir[$peerDir]") + Timber.i("NbStore connecting... peerDir[$peerDir].") peerDir.mkdirs() val db = peerDir.resolve("$spaceId.db") docStoragePool.connect(id, db.path) + Timber.i("NbStore connected [ id = $id ].") call.resolve() } catch (e: Exception) { - call.reject("Failed to connect storage", e) + Timber.e(e, "Failed to connect NbStore.") + call.reject("Failed to connect NbStore.", e) } } } @@ -55,9 +58,11 @@ class NbStorePlugin : Plugin() { try { val id = call.getStringEnsure("id") docStoragePool.disconnect(universalId = id) + Timber.i("NbStore disconnected [ id = $id ].") call.resolve() } catch (e: Exception) { - call.reject("Failed to disconnect, ${e.message}", null, e) + Timber.e(e, "Failed to disconnect NbStore") + call.reject("Failed to disconnect NbStore", null, e) } } } @@ -69,8 +74,10 @@ class NbStorePlugin : Plugin() { val id = call.getStringEnsure("id") val spaceId = call.getStringEnsure("spaceId") docStoragePool.setSpaceId(universalId = id, spaceId = spaceId) + Timber.i("Set space id: [ id = $id, spaceId = $spaceId ].") call.resolve() } catch (e: Exception) { + Timber.e(e, "Failed to set space id.") call.reject("Failed to set space id, ${e.message}", null, e) } } diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/repo/GraphQLRepo.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/repo/GraphQLRepo.kt index 151d1d27b9..588774770d 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/repo/GraphQLRepo.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/repo/GraphQLRepo.kt @@ -1,6 +1,6 @@ package app.affine.pro.repo -import app.affine.pro.ai.Prompt +import app.affine.pro.Prompt import app.affine.pro.service.GraphQLClient import com.affine.pro.graphql.CreateCopilotMessageMutation import com.affine.pro.graphql.CreateCopilotSessionMutation diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt index becef5727b..14c4d91207 100644 --- a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/service/OkHttp.kt @@ -1,6 +1,14 @@ package app.affine.pro.service import androidx.core.net.toUri +import app.affine.pro.AffineApp +import app.affine.pro.utils.dataStore +import app.affine.pro.utils.set +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl @@ -26,7 +34,6 @@ object OkHttp { } }) .addInterceptor(HttpLoggingInterceptor { msg -> - Timber.tag("Affine-Network") Timber.d(msg) }.apply { level = HttpLoggingInterceptor.Level.BODY @@ -37,10 +44,23 @@ object OkHttp { object CookieStore { + const val AFFINE_SESSION = "affine_session" + const val AFFINE_USER_ID = "affine_user_id" + private val _cookies = ConcurrentHashMap>() fun saveCookies(host: String, cookies: List) { _cookies[host] = cookies + MainScope().launch(Dispatchers.IO) { + cookies.find { it.name == AFFINE_SESSION }?.let { + AffineApp.context().dataStore.set(AFFINE_SESSION, it.toString()) + } + cookies.find { it.name == AFFINE_USER_ID }?.let { + Timber.d("Update user id [${it.value}]") + AffineApp.context().dataStore.set(AFFINE_USER_ID, it.toString()) + Firebase.crashlytics.setUserId(it.value) + } + } } fun getCookies(host: String) = _cookies[host] ?: emptyList() @@ -49,5 +69,4 @@ object CookieStore { ?.let { _cookies[it] } ?.find { cookie -> cookie.name == name } ?.value - } \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/DataStore.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/DataStore.kt new file mode 100644 index 0000000000..ba49521ac2 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/DataStore.kt @@ -0,0 +1,30 @@ +package app.affine.pro.utils + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +val Context.dataStore: DataStore by preferencesDataStore(name = "affine") + +suspend fun DataStore.set(key: String, value: String) { + edit { + it[stringPreferencesKey(key)] = value + } +} + +suspend fun DataStore.get(key: String) = data.map { + it[stringPreferencesKey(key)] ?: "" +}.first() + +suspend fun DataStore.clear(vararg keys: String) { + edit { prefs -> + keys.forEach { key -> + prefs[stringPreferencesKey(key)] = "" + } + } +} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/logger/AffineDebugTree.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/logger/AffineDebugTree.kt new file mode 100644 index 0000000000..334e91ae19 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/logger/AffineDebugTree.kt @@ -0,0 +1,10 @@ +package app.affine.pro.utils.logger + +import timber.log.Timber + +class AffineDebugTree : Timber.DebugTree() { + + override fun createStackElementTag(element: StackTraceElement): String { + return "Affine:${super.createStackElementTag(element)}:${element.lineNumber}" + } +} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/logger/CrashlyticsTree.kt b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/logger/CrashlyticsTree.kt new file mode 100644 index 0000000000..b1652bfbd3 --- /dev/null +++ b/packages/frontend/apps/android/App/app/src/main/java/app/affine/pro/utils/logger/CrashlyticsTree.kt @@ -0,0 +1,36 @@ +package app.affine.pro.utils.logger + +import android.util.Log +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase +import timber.log.Timber + +class CrashlyticsTree : Timber.Tree() { + + private val crashlytics = Firebase.crashlytics + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + + if (priority < Log.INFO) { + return + } + + val level = when (priority) { + Log.ASSERT -> "[assert]" + Log.ERROR -> "[error]" + Log.WARN -> "[warn]" + else -> "[info]" + } + + crashlytics.log( + StringBuilder(level) + .append(tag?.let { "[$tag]" } ?: "") + .append(" ") + .append(message) + .toString() + ) + + if (t == null) return + crashlytics.recordException(t) + } +} \ No newline at end of file diff --git a/packages/frontend/apps/android/App/build.gradle b/packages/frontend/apps/android/App/build.gradle index b0f3d5766a..f3c09aaf85 100644 --- a/packages/frontend/apps/android/App/build.gradle +++ b/packages/frontend/apps/android/App/build.gradle @@ -21,15 +21,17 @@ plugins { alias libs.plugins.version.catalog.update alias libs.plugins.android.application apply false alias libs.plugins.android.library apply false + alias libs.plugins.apollo.android apply false + alias libs.plugins.compose apply false + alias libs.plugins.firebase.crashlytics apply false + alias libs.plugins.google.service apply false + alias libs.plugins.hilt apply false + alias libs.plugins.jetbrains.kotlin.jvm apply false alias libs.plugins.kotlin.android apply false alias libs.plugins.kotlin.parcelize apply false alias libs.plugins.kotlin.serialization apply false - alias libs.plugins.compose apply false alias libs.plugins.ksp apply false - alias libs.plugins.hilt apply false alias libs.plugins.rust.android apply false - alias libs.plugins.apollo.android apply false - alias libs.plugins.jetbrains.kotlin.jvm apply false } apply from: "variables.gradle" diff --git a/packages/frontend/apps/android/App/buildscripts/toml-updater-config.gradle b/packages/frontend/apps/android/App/buildscripts/toml-updater-config.gradle index 946f17e79d..4901f03a59 100644 --- a/packages/frontend/apps/android/App/buildscripts/toml-updater-config.gradle +++ b/packages/frontend/apps/android/App/buildscripts/toml-updater-config.gradle @@ -1,13 +1,11 @@ versionCatalogUpdate { - sortByKey.set(true) - - keep { - // keep versions without any library or plugin reference - keepUnusedVersions.set(true) - // keep all libraries that aren't used in the project - keepUnusedLibraries.set(true) - // keep all plugins that aren't used in the project - keepUnusedPlugins.set(true) + sortByKey = true + versionCatalogs { + special { + keep { + keepUnusedVersions = true + } + } } } diff --git a/packages/frontend/apps/android/App/gradle/libs.versions.toml b/packages/frontend/apps/android/App/gradle/libs.versions.toml index 5fbc946911..ec6a84c0a4 100644 --- a/packages/frontend/apps/android/App/gradle/libs.versions.toml +++ b/packages/frontend/apps/android/App/gradle/libs.versions.toml @@ -1,5 +1,4 @@ [versions] -# @keep android-gradle-plugin = "8.9.1" androidx-activity-compose = "1.10.1" androidx-appcompat = "1.7.0" @@ -8,6 +7,7 @@ androidx-compose-bom = "2025.04.00" androidx-coordinatorlayout = "1.3.0" androidx-core-ktx = "1.16.0" androidx-core-splashscreen = "1.0.1" +androidx-datastore-preferences = "1.1.4" androidx-espresso-core = "3.6.1" androidx-junit = "1.2.1" androidx-lifecycle-compose = "2.8.7" @@ -17,6 +17,8 @@ apollo = "4.1.1" apollo-kotlin-adapters = "0.0.4" # @keep compileSdk = "35" +firebase-bom = "33.12.0" +firebase-crashlytics = "3.0.3" google-services = "4.4.2" gradle-versions = "0.52.0" hilt = "2.56.1" @@ -31,11 +33,11 @@ ksp = "2.1.20-2.0.0" # @keep minSdk = "22" mozilla-rust-android = "0.9.6" -okhttp = "5.0.0-alpha.14" +okhttp-bom = "5.0.0-alpha.14" # @keep targetSdk = "35" timber = "5.0.1" -version-catalog-update = "0.8.5" +version-catalog-update = "1.0.0" [libraries] android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" } @@ -56,6 +58,7 @@ androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "androidx-coordinatorlayout" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore-preferences" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso-core" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-compose" } @@ -63,10 +66,13 @@ androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-v androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } -apollo-adapters-core = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-kotlin-adapters"} -apollo-adapters-kotlinx-datetime = { module = "com.apollographql.adapters:apollo-adapters-kotlinx-datetime", version.ref = "apollo-kotlin-adapters"} +apollo-adapters-core = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-kotlin-adapters" } +apollo-adapters-kotlinx-datetime = { module = "com.apollographql.adapters:apollo-adapters-kotlinx-datetime", version.ref = "apollo-kotlin-adapters" } apollo-api = { module = "com.apollographql.apollo:apollo-api", version.ref = "apollo" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } google-services = { module = "com.google.gms:google-services", version.ref = "google-services" } hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } @@ -79,8 +85,8 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } okhttp = { module = "com.squareup.okhttp3:okhttp" } +okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp-bom" } okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines" } -okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" } okhttp-sse = { module = "com.squareup.okhttp3:okhttp-sse" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } @@ -90,6 +96,8 @@ android-application = { id = "com.android.application", version.ref = "android-g android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } apollo-android = { id = "com.apollographql.apollo", version.ref = "apollo" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" } +google-service = { id = "com.google.gms.google-services", version.ref = "google-services" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }