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
9 changes: 6 additions & 3 deletions .depot/workflows/android-instrumented.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ jobs:
# rebuilt when those sources change (mirrors the Namespace libcore cache strategy).
- name: libcore status
run: |
find buildScript libcore/*.sh buildScript/lib/core/get_source_env.sh \
| sort | xargs cat | sha1sum | awk '{print $1}' > libcore_status
git ls-files libcore | sort | xargs cat | sha1sum | awk '{print $1}' >> libcore_status
# git ls-files -s emits <mode> <objecthash> <stage> <path> for every tracked
# entry (content- and path-aware) without touching the filesystem, so it is
# deterministic and safe even for dangling symlinks (e.g. buildScript/nkmr).
git ls-files -s -- buildScript libcore \
| sha256sum \
| awk '{print $1}' > libcore_status
cat libcore_status

- name: libcore cache
Expand Down
148 changes: 148 additions & 0 deletions .depot/workflows/build-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Build APK
on:
push:
branches:
- '**'
workflow_dispatch:
permissions:
contents: read
env:
GO_VERSION: '1.26.4'
NDK_VERSION: '25.0.8775105'
MIERU_VERSION: v3.34.0
HYSTERIA_VERSION: v2.9.2
MDVPN_REF: android-vpnservice-protect-hook
MDVPN_COMMIT: d481d72d4b86783a87d536c214d2c68cc4e9320e
NAIVE_VERSION: v149.0.7827.114-1
jobs:
build-apk:
name: Build OSS Debug APK
runs-on: depot-ubuntu-24.04-8
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Install SDK platform + NDK
run: sdkmanager --install "platforms;android-35" "build-tools;35.0.0" "ndk;${NDK_VERSION}"

- name: local.properties
run: |
echo "sdk.dir=${ANDROID_HOME}" > local.properties
echo "ndk.dir=${ANDROID_HOME}/ndk/${NDK_VERSION}" >> local.properties

- name: libcore status
run: |
# git ls-files -s emits <mode> <objecthash> <stage> <path> for every tracked
# entry (content- and path-aware) without touching the filesystem, so it is
# deterministic and safe even for dangling symlinks (e.g. buildScript/nkmr).
git ls-files -s -- buildScript libcore \
| sha256sum \
| awk '{print $1}' > libcore_status

- name: libcore cache
id: libcore-cache
uses: actions/cache@v4
with:
path: app/libs/libcore.aar
key: depot-libcore-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ hashFiles('libcore_status') }}

- name: sidecars status
run: |
git ls-files -s -- \
buildScript/lib/mieru.sh \
buildScript/lib/hysteria2.sh \
buildScript/lib/masterdnsvpn.sh \
buildScript/lib/naive.sh \
buildScript/init/env.sh \
buildScript/init/env_ndk.sh \
| sha256sum \
| awk '{print $1}' > sidecars_status

- name: sidecars cache
id: sidecars-cache
uses: actions/cache@v4
with:
path: app/executableSo
key: depot-sidecars-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ env.MIERU_VERSION }}-${{ env.HYSTERIA_VERSION }}-${{ env.MDVPN_COMMIT }}-${{ env.NAIVE_VERSION }}-${{ hashFiles('sidecars_status') }}

- name: Install Go
if: steps.libcore-cache.outputs.cache-hit != 'true' || steps.sidecars-cache.outputs.cache-hit != 'true'
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}

- name: Build libcore (Go + gomobile)
if: steps.libcore-cache.outputs.cache-hit != 'true'
run: ./run lib core

- name: Build sidecars
if: steps.sidecars-cache.outputs.cache-hit != 'true'
run: |
./run lib mieru
./run lib hysteria2
./run lib masterdnsvpn
./run lib naive

- name: Verify sidecar artifacts
run: |
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
for so in libmieru.so libhysteria2.so libmasterdnsvpn.so libnaive.so; do
if [ ! -f "app/executableSo/$abi/$so" ]; then
echo "Error: missing app/executableSo/$abi/$so" >&2
exit 1
fi
done
done

- name: Gradle cache
uses: actions/cache@v4
with:
path: ~/.gradle
key: depot-gradle-oss-${{ hashFiles('**/*.gradle.kts') }}

- name: Build APK
env:
BUILD_PLUGIN: none
KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }}
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
ALIAS_NAME: ${{ secrets.ALIAS_NAME }}
ALIAS_PASS: ${{ secrets.ALIAS_PASS }}
run: |
set -euo pipefail
if [ -z "${KEYSTORE_B64}" ]; then
echo "Error: signing keystore is not configured" >&2
exit 1
fi
if [ -z "${KEYSTORE_PASS}" ] || [ -z "${ALIAS_NAME}" ] || [ -z "${ALIAS_PASS}" ]; then
echo "Error: signing configuration is incomplete" >&2
exit 1
fi
./run init action gradle
echo "${KEYSTORE_B64}" | base64 -d > release.keystore
./gradlew app:assembleOssDebug
python3 - <<'PY' > apk_file
from pathlib import Path
matches = sorted(Path('app/build/outputs/renamed_apks').rglob('*arm64-v8a*.apk'))
if not matches:
raise SystemExit('Error: no arm64-v8a APK found under app/build/outputs/renamed_apks')
print(matches[0])
PY
APK_FILE=$(cat apk_file)
echo "APK_FILE=$APK_FILE" >> "$GITHUB_ENV"
echo "Built APK: $APK_FILE"

- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: NekoBox-debug-arm64-v8a-apk
path: ${{ env.APK_FILE }}
if-no-files-found: error
10 changes: 6 additions & 4 deletions .depot/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ jobs:

- name: libcore status
run: |
{ find buildScript libcore -type f -print0; \
printf '%s\0' buildScript/lib/core/get_source_env.sh; } \
| sort -z | xargs -0 cat | sha1sum | awk '{print $1}' > libcore_status
git ls-files libcore | sort | xargs cat | sha1sum | awk '{print $1}' >> libcore_status
# git ls-files -s emits <mode> <objecthash> <stage> <path> for every tracked
# entry (content- and path-aware) without touching the filesystem, so it is
# deterministic and safe even for dangling symlinks (e.g. buildScript/nkmr).
git ls-files -s -- buildScript libcore \
| sha256sum \
| awk '{print $1}' > libcore_status
- name: libcore cache
id: libcore-cache
uses: actions/cache@v4
Expand Down
10 changes: 6 additions & 4 deletions .depot/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ jobs:

- name: libcore status
run: |
{ find buildScript libcore -type f -print0; \
printf '%s\0' buildScript/lib/core/get_source_env.sh; } \
| sort -z | xargs -0 cat | sha1sum | awk '{print $1}' > libcore_status
git ls-files libcore | sort | xargs cat | sha1sum | awk '{print $1}' >> libcore_status
# git ls-files -s emits <mode> <objecthash> <stage> <path> for every tracked
# entry (content- and path-aware) without touching the filesystem, so it is
# deterministic and safe even for dangling symlinks (e.g. buildScript/nkmr).
git ls-files -s -- buildScript libcore \
| sha256sum \
| awk '{print $1}' > libcore_status
- name: libcore cache
id: libcore-cache
uses: actions/cache@v4
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
name: CI
on:
push:
tags-ignore:
- 'v*'
branches:
- '*'
pull_request:
workflow_dispatch:
env:
MIERU_VERSION: v3.34.0
HYSTERIA_VERSION: v2.9.2
Comment on lines 1 to 6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unit tests no longer run automatically

ci.yml previously triggered on every push and pull_request; it now only fires via workflow_dispatch. The new Depot workflow (build-apk.yml) runs on every branch push but only assembles the APK — it contains no ./gradlew test step. The four new test files added by this PR (SubscriptionUpdaterScheduleTest, BackupFileNameTest, BackupFormatV2Test, WebDAVSecurityTest) will never run in CI unless the workflow is triggered manually. A future commit that breaks these tests will pass CI silently.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Expand Down
20 changes: 0 additions & 20 deletions app/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,27 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 9.2.1" type="baseline" client="gradle" dependencies="false" name="AGP (9.2.1)" variant="all" version="9.2.1">

<issue
id="MissingPermission"
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
errorLine1=" NotificationManagerCompat.from(service as Service).notify(notificationId, it.build())"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt"
line="209"
column="9"/>
</issue>

<issue
id="MissingPermission"
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
errorLine1=" nm.notify(2, notification.build())"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt"
line="88"
column="17"/>
</issue>

<issue
id="DefaultLocale"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
package io.nekohasekai.sagernet.bg

import android.Manifest.permission.POST_NOTIFICATIONS
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
import android.os.Build
import android.text.format.Formatter
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import io.nekohasekai.sagernet.Action
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.SpeedDisplayData
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.database.SagerDatabase
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.getColorAttr
import io.nekohasekai.sagernet.ktx.runOnMainDispatcher
Expand Down Expand Up @@ -236,7 +240,21 @@ class ServiceNotification(
}

private suspend fun update() = useBuilder {
NotificationManagerCompat.from(service as Service).notify(notificationId, it.build())
val serviceContext = service as Service
val notificationManager = NotificationManagerCompat.from(serviceContext)
if (!notificationManager.areNotificationsEnabled()) return@useBuilder
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(serviceContext, POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
return@useBuilder
}
try {
notificationManager.notify(notificationId, it.build())
} catch (e: SecurityException) {
Logs.w("service notification update skipped", e)
}
}

fun destroy() {
Expand Down
Loading