mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 05:48:59 +08:00
feat(core): improve mobile perf (#15317)
#### PR Dependency Tree * **PR #15317** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Virtualized mobile navigation with shell navigation and interactive swipe menus; coordinated mobile back handling with interactive phases/state restoration. * Added shared auth request proxy and message-port based token handling across mobile and worker flows. * **Bug Fixes** * Hydrated remote worker error stacks for calls and observable errors. * Improved SQLite FTS/indexer and nbstore optional text handling; refined docs-search ref parsing and notification loading/retry. * **Refactor / UX** * Modal focus-preservation and pointer behavior updates; improved mobile menu controls and back gesture plugins. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -20,35 +20,42 @@ def nativeAbi = nativeTarget == 'x86_64' ? 'x86_64' : 'arm64-v8a'
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
android {
|
||||
namespace "app.affine.pro"
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
namespace = "app.affine.pro"
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
ndkVersion = new File(sdkDirectory, "ndk").listFiles().sort().last().name
|
||||
defaultConfig {
|
||||
applicationId "app.affine.pro"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName System.getenv('VERSION_NAME') ?: 'v1.0.0-local'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
applicationId = "app.affine.pro"
|
||||
minSdkVersion = rootProject.ext.minSdkVersion
|
||||
targetSdkVersion = rootProject.ext.targetSdkVersion
|
||||
versionCode = 1
|
||||
versionName = System.getenv('VERSION_NAME') ?: 'v1.0.0-local'
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [usesCleartextTraffic: "false"]
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
ndk {
|
||||
abiFilters nativeAbi
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
buildConfig true
|
||||
viewBinding true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
profile {
|
||||
initWith release
|
||||
profileable true
|
||||
signingConfig signingConfigs.debug
|
||||
matchingFallbacks = ['release']
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
@@ -112,6 +119,7 @@ dependencies {
|
||||
implementation libs.androidx.datastore.preferences
|
||||
implementation libs.androidx.navigation.fragment
|
||||
implementation libs.androidx.navigation.ui.ktx
|
||||
implementation libs.androidx.webkit
|
||||
|
||||
implementation libs.apollo.runtime
|
||||
implementation (libs.jna) {
|
||||
@@ -159,8 +167,8 @@ cargo {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
apiVersion = KotlinVersion.KOTLIN_2_1
|
||||
languageVersion = KotlinVersion.KOTLIN_2_1
|
||||
apiVersion = KotlinVersion.KOTLIN_2_2
|
||||
languageVersion = KotlinVersion.KOTLIN_2_2
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package app.affine.pro
|
||||
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MobileBackDispatcherTest {
|
||||
@Test
|
||||
fun disabledAppCallbackPassesTheSameBackToTheNextCallback() {
|
||||
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
var fallbackCalled = false
|
||||
activity.onBackPressedDispatcher.addCallback(
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
fallbackCalled = true
|
||||
}
|
||||
},
|
||||
)
|
||||
val appCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() = Unit
|
||||
}
|
||||
activity.onBackPressedDispatcher.addCallback(appCallback)
|
||||
|
||||
appCallback.isEnabled = false
|
||||
activity.onBackPressedDispatcher.onBackPressed()
|
||||
|
||||
assertTrue(fallbackCalled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun predictiveLifecycleTargetsOneEnabledCallback() {
|
||||
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
||||
scenario.onActivity { activity ->
|
||||
val phases = mutableListOf<String>()
|
||||
activity.onBackPressedDispatcher.addCallback(
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackStarted(backEvent: BackEventCompat) {
|
||||
phases.add("begin")
|
||||
}
|
||||
|
||||
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
|
||||
phases.add("progress")
|
||||
}
|
||||
|
||||
override fun handleOnBackCancelled() {
|
||||
phases.add("cancel")
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
phases.add("commit")
|
||||
}
|
||||
},
|
||||
)
|
||||
val event = BackEventCompat(0f, 0f, 0.5f, BackEventCompat.EDGE_LEFT)
|
||||
activity.onBackPressedDispatcher.dispatchOnBackStarted(event)
|
||||
activity.onBackPressedDispatcher.dispatchOnBackProgressed(event)
|
||||
activity.onBackPressedDispatcher.dispatchOnBackCancelled()
|
||||
activity.onBackPressedDispatcher.dispatchOnBackStarted(event)
|
||||
activity.onBackPressedDispatcher.onBackPressed()
|
||||
|
||||
assertEquals(
|
||||
listOf("begin", "progress", "cancel", "begin", "commit"),
|
||||
phases,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:label="@string/title_activity_main"
|
||||
|
||||
@@ -3,6 +3,12 @@ package app.affine.pro
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewOutcomeReceiver
|
||||
import androidx.webkit.WebViewStartUpConfig
|
||||
import androidx.webkit.WebViewStartUpResult
|
||||
import androidx.webkit.WebViewStartupException
|
||||
import app.affine.pro.utils.logger.AffineDebugTree
|
||||
import app.affine.pro.utils.logger.CrashlyticsTree
|
||||
import app.affine.pro.utils.logger.FileTree
|
||||
@@ -11,12 +17,14 @@ import com.google.firebase.crashlytics.setCustomKeys
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@HiltAndroidApp
|
||||
class AFFiNEApp : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startWebView()
|
||||
_context = applicationContext
|
||||
// init logger
|
||||
if (BuildConfig.DEBUG) {
|
||||
@@ -33,6 +41,30 @@ class AFFiNEApp : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWebView() {
|
||||
val startedAt = SystemClock.elapsedRealtime()
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val config = WebViewStartUpConfig.Builder(executor).build()
|
||||
WebViewCompat.startUpWebView(
|
||||
this,
|
||||
config,
|
||||
object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {
|
||||
override fun onResult(result: WebViewStartUpResult) {
|
||||
executor.shutdown()
|
||||
Timber.i(
|
||||
"WebView startup completed asynchronously in %d ms.",
|
||||
SystemClock.elapsedRealtime() - startedAt,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onError(error: WebViewStartupException) {
|
||||
executor.shutdown()
|
||||
Timber.w(error, "WebView asynchronous startup failed.")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
_context = null
|
||||
super.onTerminate()
|
||||
@@ -44,4 +76,4 @@ class AFFiNEApp : Application() {
|
||||
|
||||
fun context() = requireNotNull(_context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ object AuthInitializer {
|
||||
fun initialize(bridge: Bridge) {
|
||||
bridge.addWebViewListener(object : WebViewListener() {
|
||||
override fun onPageLoaded(webView: WebView?) {
|
||||
bridge.removeWebViewListener(this)
|
||||
webView?.post { bridge.removeWebViewListener(this) }
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
FileTree.get()?.checkAndUploadOldLogs(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package app.affine.pro
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.webkit.WebSettings
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateMargins
|
||||
@@ -19,6 +22,7 @@ import app.affine.pro.plugin.AFFiNEThemePlugin
|
||||
import app.affine.pro.plugin.AuthPlugin
|
||||
import app.affine.pro.plugin.HashCashPlugin
|
||||
import app.affine.pro.plugin.NbStorePlugin
|
||||
import app.affine.pro.plugin.MobileBackPlugin
|
||||
import app.affine.pro.plugin.PreviewPlugin
|
||||
import app.affine.pro.service.GraphQLService
|
||||
import app.affine.pro.service.SSEService
|
||||
@@ -53,6 +57,7 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AFFiNEThemePlugi
|
||||
AuthPlugin::class.java,
|
||||
HashCashPlugin::class.java,
|
||||
NbStorePlugin::class.java,
|
||||
MobileBackPlugin::class.java,
|
||||
PreviewPlugin::class.java,
|
||||
)
|
||||
)
|
||||
@@ -83,6 +88,8 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AFFiNEThemePlugi
|
||||
private var navHeight = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
|
||||
navHeight = px2dp(insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom)
|
||||
@@ -96,6 +103,16 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AFFiNEThemePlugi
|
||||
configureEditorWebView()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
|
||||
bridge.webView.evaluateJavascript(
|
||||
"window.dispatchEvent(new Event('affine:memory-pressure'))",
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureEditorWebView() {
|
||||
bridge.webView.apply {
|
||||
overScrollMode = View.OVER_SCROLL_NEVER
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package app.affine.pro.plugin
|
||||
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
|
||||
@CapacitorPlugin(name = "MobileBack")
|
||||
class MobileBackPlugin : Plugin() {
|
||||
private val callback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackStarted(backEvent: BackEventCompat) {
|
||||
emit("begin", backEvent.progress)
|
||||
}
|
||||
|
||||
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
|
||||
emit("progress", backEvent.progress)
|
||||
}
|
||||
|
||||
override fun handleOnBackCancelled() {
|
||||
emit("cancel")
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
emit("commit")
|
||||
}
|
||||
}
|
||||
|
||||
override fun load() {
|
||||
activity.onBackPressedDispatcher.addCallback(activity, callback)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun setEnabled(call: PluginCall) {
|
||||
callback.isEnabled = call.getBoolean("enabled", false) ?: false
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
private fun emit(phase: String, progress: Float? = null) {
|
||||
notifyListeners("back", JSObject().apply {
|
||||
put("phase", phase)
|
||||
progress?.let { put("progress", it) }
|
||||
})
|
||||
}
|
||||
|
||||
override fun handleOnDestroy() {
|
||||
callback.remove()
|
||||
super.handleOnDestroy()
|
||||
}
|
||||
}
|
||||
+9
-6
@@ -7,11 +7,14 @@ import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import uniffi.affine_mobile_native.DocRecord
|
||||
import uniffi.affine_mobile_native.SetBlob
|
||||
import uniffi.affine_mobile_native.newDocStoragePool
|
||||
|
||||
private const val ANDROID_INDEXER_VERSION_OFFSET = 1u
|
||||
|
||||
@CapacitorPlugin(name = "NbStoreDocStorage")
|
||||
class NbStorePlugin : Plugin() {
|
||||
|
||||
@@ -598,10 +601,10 @@ class NbStorePlugin : Plugin() {
|
||||
JSObject()
|
||||
.put("blockId", block.blockId)
|
||||
.put("flavour", block.flavour)
|
||||
.put("content", block.content)
|
||||
.put("blob", block.blob)
|
||||
.put("refDocId", block.refDocId)
|
||||
.put("refInfo", block.refInfo)
|
||||
.put("content", block.content?.let(::JSArray))
|
||||
.put("blob", block.blob?.let(::JSArray))
|
||||
.put("refDocId", block.refDocId?.let(::JSArray))
|
||||
.put("refInfo", block.refInfo?.let(::JSArray))
|
||||
.put("parentFlavour", block.parentFlavour)
|
||||
.put("parentBlockId", block.parentBlockId)
|
||||
.put("additional", block.additional)
|
||||
@@ -683,7 +686,7 @@ class NbStorePlugin : Plugin() {
|
||||
val indexName = call.getStringEnsure("indexName")
|
||||
val docId = call.getStringEnsure("docId")
|
||||
val text = docStoragePool.ftsGetDocument(id, indexName, docId)
|
||||
call.resolve(JSObject().put("text", text))
|
||||
call.resolve(JSObject().put("text", text ?: JSONObject.NULL))
|
||||
} catch (e: Exception) {
|
||||
call.reject("Failed to get fts document: ${e.message}", null, e)
|
||||
}
|
||||
@@ -728,7 +731,7 @@ class NbStorePlugin : Plugin() {
|
||||
fun ftsIndexVersion(call: PluginCall) {
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
val version = docStoragePool.ftsIndexVersion()
|
||||
val version = docStoragePool.ftsIndexVersion() + ANDROID_INDEXER_VERSION_OFFSET
|
||||
call.resolve(JSObject().put("indexVersion", version))
|
||||
} catch (e: Exception) {
|
||||
call.reject("Failed to get fts index version: ${e.message}", null, e)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme.NoActionBar</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user