delete CloudKit and added BalanceFlutter
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

This commit is contained in:
wt committed 2026-10-28 15:37:32 +07:00
1 parent a715eca947
commit 2c278044bb
214 files changed
+13411 -2290

No files matched your search

@@ -0,0 +1,20 @@
group 'com.balance.runtime'
version '1.0'
apply plugin: 'com.android.library'
android {
namespace 'com.balance.runtime'
compileSdk 36
defaultConfig {
minSdk 23
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}
@@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,105 @@
package com.balance.runtime;
import android.os.Handler;
import android.os.Looper;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
/** Serializes complete ledger/session operations across Flutter engines/processes. */
public final class BalanceRuntimePlugin implements FlutterPlugin, MethodChannel.MethodCallHandler {
private final Handler main = new Handler(Looper.getMainLooper());
private final Object state = new Object();
private MethodChannel channel;
private ExecutorService executor;
private File lockFile;
private boolean detached = true;
private boolean acquiring;
private LedgerFileLock held;
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
synchronized (state) {
detached = false;
acquiring = false;
lockFile = new File(binding.getApplicationContext().getFilesDir(), "balance-runtime.lock");
executor = Executors.newSingleThreadExecutor();
}
channel = new MethodChannel(binding.getBinaryMessenger(), "balance/runtime");
channel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if ("acquireLock".equals(call.method)) {
synchronized (state) {
if (detached || acquiring || held != null) {
result.error("lock_busy", "This engine already owns or is waiting for the ledger lock", null);
return;
}
acquiring = true;
executor.execute(() -> acquire(result));
}
} else if ("releaseLock".equals(call.method)) {
LedgerFileLock release;
synchronized (state) {
String token = call.argument("token");
if (held == null || !held.token.equals(token)) {
result.error("lock_owner", "Ledger lock token does not match this engine", null);
return;
}
release = held;
held = null;
}
// Never queue release behind another engine's blocking acquisition.
release.close();
result.success(null);
} else {
result.notImplemented();
}
}
private void acquire(MethodChannel.Result result) {
try {
LedgerFileLock acquired = LedgerFileLock.acquire(lockFile);
synchronized (state) {
acquiring = false;
if (detached) {
acquired.close();
return;
}
held = acquired;
}
main.post(() -> {
synchronized (state) {
if (detached || held != acquired) return;
result.success(acquired.token);
}
});
} catch (Exception error) {
synchronized (state) { acquiring = false; }
main.post(() -> {
synchronized (state) {
if (!detached) result.error("lock_failed", "Unable to acquire the local ledger lock", null);
}
});
}
}
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
LedgerFileLock release;
synchronized (state) {
detached = true;
release = held;
held = null;
if (executor != null) executor.shutdownNow();
}
if (channel != null) channel.setMethodCallHandler(null);
if (release != null) release.close();
}
}
@@ -0,0 +1,55 @@
package com.balance.runtime;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.UUID;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
/** OS lock plus JVM serialization, independent of any Flutter engine lifetime. */
final class LedgerFileLock implements AutoCloseable {
// FileChannel alone throws OverlappingFileLockException in one JVM.
private static final Semaphore PROCESS_LOCK = new Semaphore(1, true);
final String token = UUID.randomUUID().toString();
private final RandomAccessFile file;
private final FileChannel channel;
private final FileLock lock;
private final AtomicBoolean closed = new AtomicBoolean();
private LedgerFileLock(RandomAccessFile file, FileChannel channel, FileLock lock) {
this.file = file;
this.channel = channel;
this.lock = lock;
}
static LedgerFileLock acquire(File path) throws IOException, InterruptedException {
PROCESS_LOCK.acquire();
RandomAccessFile file = null;
FileChannel channel = null;
FileLock lock = null;
try {
file = new RandomAccessFile(path, "rw");
channel = file.getChannel();
lock = channel.lock();
return new LedgerFileLock(file, channel, lock);
} catch (IOException | RuntimeException error) {
try { if (lock != null) lock.release(); } catch (IOException ignored) {}
try { if (channel != null) channel.close(); } catch (IOException ignored) {}
try { if (file != null) file.close(); } catch (IOException ignored) {}
PROCESS_LOCK.release();
throw error;
}
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) return;
try { lock.release(); } catch (IOException ignored) {}
try { channel.close(); } catch (IOException ignored) {}
try { file.close(); } catch (IOException ignored) {}
PROCESS_LOCK.release();
}
}
@@ -0,0 +1,86 @@
package com.balance.runtime;
import static org.junit.Assert.*;
import java.io.File;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
public class LedgerFileLockTest {
@Test
public void separateProcessWaitsForOsFileLock() throws Exception {
File path = Files.createTempFile("balance-process-lock", ".lock").toFile();
Process child = null;
try (LedgerFileLock first = LedgerFileLock.acquire(path)) {
String classpath = new File(LedgerLockProbe.class.getProtectionDomain()
.getCodeSource().getLocation().toURI()).getPath()
+ File.pathSeparator + new File(LedgerFileLock.class.getProtectionDomain()
.getCodeSource().getLocation().toURI()).getPath();
child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").getPath(),
"-cp", classpath, LedgerLockProbe.class.getName(), path.getPath())
.redirectErrorStream(true).start();
BufferedReader output = new BufferedReader(new InputStreamReader(child.getInputStream()));
assertEquals("waiting", output.readLine());
assertFalse(child.waitFor(100, TimeUnit.MILLISECONDS));
first.close();
assertTrue(child.waitFor(3, TimeUnit.SECONDS));
assertEquals("acquired", output.readLine());
assertEquals(0, child.exitValue());
} finally {
if (child != null && child.isAlive()) child.destroyForcibly();
path.delete();
}
}
@Test
public void concurrentOwnersWaitAndReleaseIsIdempotent() throws Exception {
File path = Files.createTempFile("balance-lock", ".lock").toFile();
LedgerFileLock first = LedgerFileLock.acquire(path);
CountDownLatch waiting = new CountDownLatch(1);
CountDownLatch entered = new CountDownLatch(1);
Thread second = new Thread(() -> {
waiting.countDown();
try (LedgerFileLock ignored = LedgerFileLock.acquire(path)) {
entered.countDown();
} catch (Exception error) { throw new AssertionError(error); }
});
second.start();
assertTrue(waiting.await(2, TimeUnit.SECONDS));
assertFalse(entered.await(100, TimeUnit.MILLISECONDS));
first.close();
first.close();
assertTrue(entered.await(2, TimeUnit.SECONDS));
second.join(2000);
assertFalse(second.isAlive());
path.delete();
}
@Test
public void interruptedWaiterDoesNotLeakOrReleaseAnotherOwner() throws Exception {
File path = Files.createTempFile("balance-lock", ".lock").toFile();
LedgerFileLock first = LedgerFileLock.acquire(path);
CountDownLatch waiting = new CountDownLatch(1);
AtomicBoolean interrupted = new AtomicBoolean();
Thread waiter = new Thread(() -> {
waiting.countDown();
try (LedgerFileLock ignored = LedgerFileLock.acquire(path)) {
fail("Interrupted waiter must not acquire");
} catch (InterruptedException expected) { interrupted.set(true); }
catch (Exception error) { throw new AssertionError(error); }
});
waiter.start();
assertTrue(waiting.await(2, TimeUnit.SECONDS));
waiter.interrupt();
waiter.join(2000);
assertTrue(interrupted.get());
first.close();
try (LedgerFileLock after = LedgerFileLock.acquire(path)) {
assertNotNull(after.token);
}
path.delete();
}
}
@@ -0,0 +1,14 @@
package com.balance.runtime;
import java.io.File;
/** Separate JVM helper used to exercise the actual OS-level file lock. */
public final class LedgerLockProbe {
public static void main(String[] args) throws Exception {
System.out.println("waiting");
System.out.flush();
try (LedgerFileLock ignored = LedgerFileLock.acquire(new File(args[0]))) {
System.out.println("acquired");
}
}
}
@@ -0,0 +1,2 @@
/// The Android plugin registers the balance/runtime channel in every engine.
library;
@@ -0,0 +1,16 @@
name: balance_runtime
description: Shared Android ledger lock for foreground and background Flutter engines.
version: 1.0.0
publish_to: none
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: '>=3.22.0'
dependencies:
flutter:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.balance.runtime
pluginClass: BalanceRuntimePlugin