Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Repository Guidelines

## Project Structure & Module Organization

Application code lives in `src/`: `domain/` holds core models, `application/` holds workflows and ports, `infrastructure/` implements adapters, `presentation/` builds the HuxerUI interface, and `app/` wires services together. Android Gradle configuration and Java platform bridges live in `platform/android/`; Windows packaging lives in `platform/windows/`. Put localized text in `resources/strings/`, images in `resources/images/`, native tests in `tests/`, and development scripts in `tools/`.

## Build, Test, and Development Commands

Use HuxerUI SDK 0.3.0. Android builds also need JDK 17, Android SDK 36, NDK `29.0.14206865`, CMake, and Ninja.

```sh
cmake -S tests -B build/tests-ninja -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/tests-ninja --parallel 8
ctest --test-dir build/tests-ninja --output-on-failure -j8
(cd tools && python3 -m unittest test_fake_ai_server.py -v)
./platform/android/gradlew -p platform/android :app:lintDebug :app:assembleDebug --max-workers=8
```

The first three commands configure, build, and run native GoogleTest targets. The Python command checks the fake AI server; Gradle runs Android lint and produces a debug APK. Signed release builds require a local `platform/android/signing.properties`.

## Release Notes

`update.md` is the versioned Chinese changelog. Before a release, add a `## v<versionName>` section matching Android's `versionName`. `.github/workflows/release.yml` extracts that section for GitHub release notes; if absent, it falls back to recent commits.

## Coding Style & Naming Conventions

Write application code in C++23; use Java only for Android APIs that require a platform bridge. Follow nearby formatting: C++ uses two-space indentation, Java uses four. Use `snake_case` C++ filenames (for example, `project_workspace_service.cpp`), descriptive PascalCase types, and `snake_case` resource keys. Keep domain, application, infrastructure, and presentation responsibilities separate; depend on ports across boundaries. No repository-wide C++ formatter is configured; Android Lint and native compiler warnings are enforced in verification.

## Testing Guidelines

Add focused GoogleTest coverage for behavior changes in `tests/*_tests.cpp`. Put Android JVM tests in `platform/android/app/src/test/` with `*Test.java` names when Java bridge behavior changes. Run the relevant suite before opening a PR, and verify UI changes on a device or emulator when available. Keep user-facing strings in both `default.properties` and `zh.properties`.

## Commit & Pull Request Guidelines

Recent commits use short prefixes such as `feat:`, `fix:`, `test:`, `docs:`, `ci:`, and `release:` followed by an imperative summary. Use `.github/pull_request_template.md`: link the issue, describe visible behavior, record validation commands, and include before/after screenshots for UI changes. Complete its CLA checkbox and `CLA Signature: @your-github-username` with the PR author's exact GitHub login. Never commit credentials, signing keys, user data, or generated build output.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ if (NOT HUXERUI_LIBRARY_GRAPH_ONLY)
linecode_enable_ssh(linecodepro)
target_include_directories(linecodepro PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(linecodepro PRIVATE ZLIB::ZLIB)
if (ANDROID)
target_link_libraries(linecodepro PRIVATE log)
endif ()
target_compile_features(linecodepro PRIVATE cxx_std_23)
if (WIN32)
target_link_libraries(linecodepro PRIVATE shell32)
Expand Down
5 changes: 3 additions & 2 deletions platform/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ android {
applicationId = "cn.lineai"
minSdk = huxeruiMinSdk
targetSdk = huxeruiTargetSdk
versionCode = 35
versionName = "1.3.1"
versionCode = 36
versionName = "1.3.2"

externalNativeBuild {
cmake {
Expand Down Expand Up @@ -218,6 +218,7 @@ androidComponents.onVariants(androidComponents.selector().all()) { variant ->
}

dependencies {
testImplementation 'junit:junit:4.13.2'
if (rootProject.gradle.ext.huxeruiUsesSource) {
implementation project(":HuxerUI")
} else {
Expand Down
76 changes: 76 additions & 0 deletions platform/android/app/src/main/java/cn/lineai/LineCodeLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cn.lineai;

import android.content.Context;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicLong;

/** Records exception locations without persisting messages or user content. */
public final class LineCodeLogger {
private static final String TAG = "LineCodePro";
private static final AtomicLong NEXT_ID = new AtomicLong();
private static boolean installed;

private LineCodeLogger() { }

public static synchronized void install(Context context) {
if (installed) {
return;
}
Context application = context.getApplicationContext();
Thread.UncaughtExceptionHandler previous =
Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((thread, error) -> {
try {
record(application, "uncaught_exception", error);
} catch (RuntimeException loggingFailure) {
Log.e(TAG, "Failed to record uncaught exception");
} finally {
if (previous != null) {
previous.uncaughtException(thread, error);
}
}
});
installed = true;
}

public static void record(Context context, String event, Throwable error) {
String kind = error == null ? "unknown" : error.getClass().getName();
Log.e(TAG, event + " (" + kind + ")");
try {
File directory = new File(context.getFilesDir(), "error_logs");
if (!directory.isDirectory() && !directory.mkdirs()) {
return;
}
File file = new File(directory, "java-" + System.currentTimeMillis()
+ "-" + NEXT_ID.getAndIncrement() + ".log");
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(file), StandardCharsets.UTF_8)) {
writer.write("Java ");
writer.write(event);
writer.write(' ');
writer.write(kind);
writer.write('\n');
if (error != null) {
StackTraceElement[] frames = error.getStackTrace();
for (int i = 0; i < Math.min(frames.length, 64); i++) {
StackTraceElement frame = frames[i];
writer.write(frame.getClassName());
writer.write('.');
writer.write(frame.getMethodName());
writer.write(':');
writer.write(Integer.toString(frame.getLineNumber()));
writer.write('\n');
}
}
}
} catch (IOException | RuntimeException failure) {
Log.e(TAG, "Failed to persist exception location");
}
}
}
40 changes: 4 additions & 36 deletions platform/android/app/src/main/java/cn/lineai/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package cn.lineai;

import android.graphics.Color;
import android.graphics.Insets;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowMetrics;

import org.huxerui.HuxerUIActivity;

Expand All @@ -20,46 +17,17 @@ public final class MainActivity extends HuxerUIActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
LineCodeLogger.install(this);
super.onCreate(savedInstanceState);
configureWindowChrome();
}

@SuppressWarnings("deprecation")
public float[] lineCodeWindowInsetsDp() {
int top = 0;
int right = 0;
int bottom = 0;
int left = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowMetrics metrics = getWindowManager().getCurrentWindowMetrics();
Insets insets = metrics.getWindowInsets().getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
top = insets.top;
right = insets.right;
bottom = insets.bottom;
left = insets.left;
} else {
View decor = getWindow().getDecorView();
android.view.WindowInsets rootInsets = decor.getRootWindowInsets();
if (rootInsets != null) {
top = rootInsets.getStableInsetTop();
right = rootInsets.getStableInsetRight();
bottom = rootInsets.getStableInsetBottom();
left = rootInsets.getStableInsetLeft();
}
}
float density = getResources().getDisplayMetrics().density;
return new float[]{top / density, right / density, bottom / density, left / density};
}

@SuppressWarnings("deprecation")
private void configureWindowChrome() {
Window window = getWindow();
// Immersive: HuxerUI already renders edge-to-edge, so the bars stay
// transparent and the app draws the background behind them. Every
// interactive surface pads the insets the activity reports through
// lineCodeWindowInsetsDp(), which is why content is never covered.
window.setStatusBarColor(Color.TRANSPARENT);
// HuxerUI owns the safe area; give the status bar an opaque backdrop
// so page content never appears beneath its icons during startup.
window.setStatusBarColor(DEFAULT_BACKGROUND);
window.setNavigationBarColor(Color.TRANSPARENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cn.lineai.skillhub;

import cn.lineai.LineCodeLogger;

import android.content.Context;
import android.content.SharedPreferences;
import android.webkit.CookieManager;
Expand Down Expand Up @@ -37,10 +39,12 @@ HuxerUIPlatformChannel.Cancellation invoke(
() -> { };

private final CookieManager cookies;
private final Context context;
private final SharedPreferences legacyReading;
private final Map<String, Invocation> invocations = new HashMap<>();

Module(Context context) {
this.context = context;
cookies = CookieManager.getInstance();
legacyReading = context.getSharedPreferences(
"skill_store_reading", Context.MODE_PRIVATE);
Expand All @@ -67,6 +71,7 @@ public HuxerUIPlatformChannel.Cancellation invoke(
try {
return invocation.invoke(arguments, result);
} catch (RuntimeException error) {
LineCodeLogger.record(context, "skill_hub_platform_call", error);
String message = error.getMessage();
result.fail(
"linecode/skill-hub/android-error",
Expand Down
Loading
Loading