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
147 changes: 78 additions & 69 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,48 +68,51 @@ jobs:
fi

ci_runs_url="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${release_commit}&event=push&status=completed&per_page=100"
curl --fail --silent --show-error \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer $GH_TOKEN" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"$ci_runs_url" > "$RUNNER_TEMP/ci-runs.json"
gh api "$ci_runs_url" > "$RUNNER_TEMP/ci-runs.json"
ci_run_id=$(jq --exit-status --raw-output --arg commit "$release_commit" '
[.workflow_runs[] | select(.head_sha == $commit and .event == "push" and .head_branch == "main" and .status == "completed")]
| max_by(.run_started_at) | select(.conclusion == "success") | .id
' "$RUNNER_TEMP/ci-runs.json") || {
echo "ERROR: Android CI push run for $release_commit on main has not completed successfully"
exit 1
}
curl --fail --silent --show-error \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer $GH_TOKEN" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${ci_run_id}/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/ci-jobs.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${ci_run_id}/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/ci-jobs.json"
if ! jq --exit-status '
any(.jobs[]; .name == "Build & Test" and .status == "completed" and .conclusion == "success")
' "$RUNNER_TEMP/ci-jobs.json" > /dev/null; then
echo "ERROR: Android CI Build & Test job for $release_commit has not completed successfully"
exit 1
fi

release_url="https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}"
release_status=$(curl --silent --show-error --output "$RUNNER_TEMP/existing-release.json" --write-out '%{http_code}' \
--header 'Authorization: Bearer $GH_TOKEN' \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"$release_url")
case "$release_status" in
404) ;;
200)
jq --exit-status --raw-output '.body | strings' "$RUNNER_TEMP/existing-release.json" > "$RUNNER_TEMP/existing-release-body.md"
echo "ERROR: release for $GITHUB_REF_NAME already exists; refusing to overwrite its notes or assets"
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" > "$RUNNER_TEMP/existing-release.json" 2> "$RUNNER_TEMP/existing-release-error.txt"; then
if ! jq --exit-status --arg tag "$GITHUB_REF_NAME" '
.draft == true and .tag_name == $tag and (.assets | length) == 0
' "$RUNNER_TEMP/existing-release.json" > /dev/null; then
echo "ERROR: release for $GITHUB_REF_NAME already exists and is not an empty recoverable draft"
exit 1
;;
*)
echo "ERROR: unable to query existing release for $GITHUB_REF_NAME (HTTP $release_status)"
fi
echo "Recovering empty draft release for $GITHUB_REF_NAME"
elif ! grep --fixed-strings --quiet 'HTTP 404' "$RUNNER_TEMP/existing-release-error.txt"; then
cat "$RUNNER_TEMP/existing-release-error.txt"
echo "ERROR: unable to query existing release for $GITHUB_REF_NAME"
exit 1
fi

gh api "repos/${GITHUB_REPOSITORY}/releases/latest" > "$RUNNER_TEMP/latest-release.json"
jq --exit-status --raw-output '.body | strings' "$RUNNER_TEMP/latest-release.json" > "$RUNNER_TEMP/latest-release-body.md"
for heading in '## 🧪 Verification' '## 📥 Installation' '## 📱 Compatibility'; do
grep --fixed-strings --quiet "$heading" "$RUNNER_TEMP/latest-release-body.md" || {
echo "ERROR: latest release notes missing expected structure: $heading"
exit 1
;;
esac
}
done

notes_source="docs/RELEASE_NOTES_${GITHUB_REF_NAME#v}.md"
[[ -s "$notes_source" ]] || { echo "ERROR: missing prepared release notes: $notes_source"; exit 1; }
grep --fixed-strings --quiet "## [${GITHUB_REF_NAME#v}]" CHANGELOG.md || {
echo "ERROR: CHANGELOG.md missing release section for ${GITHUB_REF_NAME#v}"
exit 1
}

release:
name: Build & Publish Release APK
Expand Down Expand Up @@ -141,11 +144,14 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run resource contracts
run: python scripts/test_lint_resource_contracts.py

- name: Run Debug Unit Tests
run: ./gradlew testDebugUnitTest --no-daemon

- name: Run Debug Lint
run: ./gradlew lintDebug --no-daemon
- name: Run Debug Lint and assemble APK
run: ./gradlew lintDebug assembleDebug --no-daemon

- name: Validate signing environment
env:
Expand Down Expand Up @@ -195,51 +201,54 @@ jobs:
release_apk_name="FlowPilot-${GITHUB_REF_NAME}.apk"
release_apk_path="$release_dir/$release_apk_name"
mv "$release_apk" "$release_apk_path"
release_checksum_path="${release_apk_path}.sha256"
sha256sum "$release_apk_path" > "$release_checksum_path"
sha256sum --check "$release_checksum_path"
release_sha256=$(awk '{print $1}' "$release_checksum_path")

release_title="FlowPilot ${GITHUB_REF_NAME}"
release_notes_path="$RUNNER_TEMP/release-notes.md"
cat > "$release_notes_path" <<EOF
Signed Android APK release for FlowPilot.

---

## ✨ Highlights
build_tools_dir="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}/build-tools/36.0.0"
aapt2="$build_tools_dir/aapt2"
apksigner="$build_tools_dir/apksigner"
[[ -x "$aapt2" && -x "$apksigner" ]] || { echo 'ERROR: required Android build tools unavailable'; exit 1; }

- Signed release APK built from current \`main\` commit \`${RELEASE_COMMIT}\`.
package_line=$("$aapt2" dump badging "$release_apk_path" | sed -n '1p')
[[ "$package_line" == *"name='com.flowpilot.app'"* ]] || { echo 'ERROR: APK package ID mismatch'; exit 1; }
[[ "$package_line" == *"versionCode='4'"* ]] || { echo 'ERROR: APK versionCode mismatch'; exit 1; }
[[ "$package_line" == *"versionName='1.1.0'"* ]] || { echo 'ERROR: APK versionName mismatch'; exit 1; }
"$apksigner" verify --verbose "$release_apk_path"
signer_digest=$("$apksigner" verify --print-certs "$release_apk_path" | sed -nE 's/^Signer #1 certificate SHA-256 digest: ([0-9a-fA-F]+)$/\1/p' | tr '[:upper:]' '[:lower:]')
expected_signer_digest='f1e45d3a669954ddfb858d5d3a964a2fa7ba3d00bcb0e65b5ced1ca6d5a79f2b'
[[ "$signer_digest" == "$expected_signer_digest" ]] || { echo 'ERROR: APK signer certificate mismatch'; exit 1; }

## 🧪 Verification

- Required Android CI \`Build & Test\` completed successfully for this exact commit.
- Release workflow completed debug unit tests, debug lint, and signed APK assembly.

## 📥 Installation

1. Download \`${release_apk_name}\`.
2. Verify checksum before installation.
3. Install APK on Android 8.0 (API 26) or newer.

### SHA-256 Checksum

\`\`\`
SHA256 (${release_apk_name}) = ${release_sha256}
\`\`\`

## 📱 Compatibility

- Android 8.0+ (API 26–36).

---

## 🇹🇷 Türkçe Özet

İmzalı FlowPilot APK sürümü hazır. Kurulumdan önce SHA-256 sağlama toplamını doğrulayın.
EOF
release_checksum_path="${release_apk_path}.sha256"
(
cd "$release_dir"
sha256sum "$release_apk_name" > "${release_apk_name}.sha256"
sha256sum --check "${release_apk_name}.sha256"
)
release_sha256=$(awk '{print $1}' "$release_checksum_path")

for heading in '## ✨ Highlights' '## 🧪 Verification' '## 📥 Installation' '### SHA-256 Checksum' '## 📱 Compatibility' '## 🇹🇷 Türkçe Özet'; do
release_title="FlowPilot ${GITHUB_REF_NAME} — Safer Automations, Conflict Warnings & Privacy Hardening"
release_notes_path="$RUNNER_TEMP/release-notes.md"
notes_source="docs/RELEASE_NOTES_${GITHUB_REF_NAME#v}.md"
[[ -s "$notes_source" ]] || { echo "ERROR: missing prepared release notes: $notes_source"; exit 1; }
python3 - "$notes_source" "$release_notes_path" "$release_apk_name" "$release_sha256" "$RELEASE_COMMIT" <<'PY'
from pathlib import Path
import sys

source, target, apk_name, digest, commit = sys.argv[1:]
notes = Path(source).read_text(encoding="utf-8")
replacements = {
"{{APK_NAME}}": apk_name,
"{{SHA256}}": digest,
"{{RELEASE_COMMIT}}": commit,
}
for marker, value in replacements.items():
if marker not in notes:
raise SystemExit(f"ERROR: release notes missing marker {marker}")
notes = notes.replace(marker, value)
if "{{" in notes or "}}" in notes:
raise SystemExit("ERROR: unresolved release-note marker")
Path(target).write_text(notes, encoding="utf-8")
PY

for heading in '## ✨ Safer Rule Management' '## 🛡️ Automation Security' '## 🔒 History Privacy' '## 🌐 Language & Background Notifications' '## 🧪 Verification' '## 📥 Installation' '### SHA-256 Checksum' '## 📱 Compatibility' '## 🇹🇷 Türkçe Özet'; do
grep --fixed-strings --quiet "$heading" "$release_notes_path" || { echo "ERROR: release notes missing $heading"; exit 1; }
done
grep --fixed-strings --quiet "SHA256 (${release_apk_name}) = ${release_sha256}" "$release_notes_path" || { echo 'ERROR: release notes checksum mismatch'; exit 1; }
Expand Down
19 changes: 12 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,33 @@ All notable FlowPilot changes are documented here.

## [Unreleased]

Changes completed after `1.0.2` and intended for the next release.
## [1.1.0] - 2026-09-15

### Added

- Safe rule duplication from the Home list: creates a disabled, immediately editable copy with a new identity and reset runtime state. Webhook secrets are decrypted and re-encrypted with fresh Android Keystore ciphertext; TTS cache files are copied independently with failure-safe cleanup.
- Non-blocking conflict warnings before saving or enabling automations: detects opposing state actions with likely/possible confidence, links to the conflicting rule for inspection, preserves the pending operation across inspection, and requires a deliberate override. Trigger overlap follows runtime wildcard semantics without exposing notification keywords or other sensitive arguments.

### Changed

- Foreground engine and startup-failure notifications now follow the persisted English, Turkish, or system-language selection across boot, service restart, process recreation, and task removal. Language changes refresh active notification text, channel metadata, and widget state immediately.
- GitHub Pages now has improved mobile navigation, browser-language selection, accessible brand navigation, modular assets, and release-aligned installation guidance.

### Security

- Background NFC discovery now opens a confirmation gate before it can run matching automations; only foreground Android ReaderMode scans execute automatically.
- Webhooks pin initial TCP connections to prevalidated public IP addresses, preserve TLS hostname verification, reject unsafe rendered headers, and use bounded HTTP/1.1 parsing.
- Sensitive SMS and notification events are accepted only while the engine is enabled, bounded and freshness-limited, and reauthorized immediately before execution.
- Automatic rule runs now use durable execution leases and revision checks, preventing cooldown bypasses and revoking queued work after rule changes.
- Release workflow now requires a current `main` commit, exact successful CI, matching version tag, and protected signing environment before signing.
- Enabled Dependabot vulnerability alerts and security update pull requests.
- Enabled secret scanning, push protection, and private vulnerability reporting for the public repository.
- Protected `main`: pull requests, a current successful `Build & Test` check, and resolved review conversations are required; force-push and branch deletion are disabled.
- Execution-history rule names, trigger snapshots, action arguments, and failure messages are sanitized before persistence and during legacy migration; raw provider errors and embedded sensitive markers are not retained.
- Release workflow now requires a current `main` commit, exact successful CI, matching version tag, detailed prepared notes, verified APK identity/signature, and signing environment before publication.
- Enabled Dependabot vulnerability alerts, security update pull requests, secret scanning, push protection, private vulnerability reporting, and protected `main` rules.

### Verification

- GitHub `Build & Test` passed after both feature branches were reconciled on `main`.
- Android instrumentation remains a manual emulator gate; physical-device validation for these two features is still pending.
- GitHub `Build & Test` passed for the merged feature and security changes.
- Physical-device validation passed for safe rule duplication, conflict warnings, background NFC confirmation, and language switching.
- Android instrumentation remains a manual emulator gate.

## [1.0.2] - 2026-09-13

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### The private, battery-first Android automation engine — without root.

Automate your device seamlessly with event-driven triggers, privileged system actions via Shizuku, and a fluid Material 3 interface. No telemetry, no cloud accounts, and zero background battery drain.
Automate your device with event-driven triggers, privileged system actions via Shizuku, and a fluid Material 3 interface. No telemetry or cloud accounts, with battery-efficient demand-driven listeners.

<br/>

Expand Down Expand Up @@ -165,7 +165,7 @@ FlowPilot listens to a rich spectrum of hardware, radio, and system events:
- **Device Flip:** Face-down on table or turned face-up (Proximity + Gravity Z-axis with 500ms debounce).
- **Shake:** Firm shake detection with configurable sensitivity slider.
- **Ambient Light:** Lux drops below or rises above target threshold.
- 📍 **Hardware Geofencing:** Enter or exit defined geographical zones using Google Play Services `GeofencingClient`. Zero idle battery drain, up to 50 persistent queued events across engine restarts, and coordinate reuse for template variables.
- 📍 **Hardware Geofencing:** Enter or exit defined geographical zones using Google Play Services `GeofencingClient`. Uses no idle CPU wake-lock, keeps up to 50 queued events across engine restarts, and reuses transition coordinates for template variables.
- 🏷️ **NFC Tags:** Instant hex UID matching on physical scans. Foreground ReaderMode scans run matching rules automatically; background Android discovery opens FlowPilot and requires explicit confirmation before any matching NFC automation runs.
- 📞 **Phone & SMS:** Call ringing, answered, outgoing dialed, call ended; SMS received with keyword, prefix, regex, or exact sender matching.
- 🔔 **Notifications:** Incoming notifications from selected apps with keyword filtering.
Expand Down Expand Up @@ -231,7 +231,7 @@ FlowPilot is engineered with an uncompromised commitment to user privacy:

- 🚫 **Zero Telemetry:** No Firebase Analytics, no Sentry, no remote crash reporters, and zero tracking SDKs.
- 📵 **No Cloud Synchronization:** Your automations, logs, and secrets never touch any third-party cloud.
- 🛡️ **Hardware Keystore Protection:** Webhook secrets, tokens, and sensitive headers are encrypted with AES-256-GCM using hardware-backed Android Keystore keys.
- 🛡️ **Android Keystore Protection:** Webhook secrets, tokens, and sensitive headers are encrypted with AES-256-GCM using Android Keystore keys, hardware-backed when supported by the device.
- 🙈 **Strict Log Sanitization:** Phone numbers, webhook credentials, and sensitive headers are masked across all UI screens and audit logs.

### Transparent Permission Disclosures
Expand Down
4 changes: 2 additions & 2 deletions README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ FlowPilot zengin bir donanım, radyo ve sistem olayı yelpazesini dinler:
- **Cihazı Çevirme:** Yüzüstü masaya konma veya tekrar çevrilme (Yakınlık + Yerçekimi Z-ekseni, 500ms kararlılık filtresi).
- **Sallama:** Hassasiyet ayarlı telefon sallama algılaması.
- **Ortam Işığı:** Lüks değerinin belirlenen sınırın altına düşmesi veya üstüne çıkması.
- 📍 **Donanım Coğrafi Çit (Geofence):** Google Play Services `GeofencingClient` ile belirlenen alana giriş/çıkış. Boşta sıfır pil tüketimi, yeniden başlatmada kaybolmayan 50 olaylık kalıcı kuyruk ve şablon değişkenlerinde doğrudan koordinat kullanımı.
- 📍 **Donanım Coğrafi Çit (Geofence):** Google Play Services `GeofencingClient` ile belirlenen alana giriş/çıkış. Boşta CPU wake-lock kullanmaz; yeniden başlatmada kaybolmayan 50 olaylık kalıcı kuyruk ve şablon değişkenlerinde doğrudan koordinat kullanımı sağlar.
- 🏷️ **NFC Etiketleri:** Fiziksel taramalarda anında hex UID eşleşmesi. Ön plandaki ReaderMode taramaları eşleşen kuralları otomatik çalıştırır; Android arka plan keşfi FlowPilot'ı açar ve eşleşen NFC otomasyonu çalışmadan önce açık onay ister.
- 📞 **Arama & SMS:** Gelen arama çalıyor, yanıtlandı, giden arama başladı, arama bitti; SMS gönderen numaraya ve kelime, önek veya regex kalıbına göre tetikleme.
- 🔔 **Bildirimler:** Seçili uygulamalardan gelen bildirimler ve anahtar kelime filtreleme.
Expand Down Expand Up @@ -231,7 +231,7 @@ FlowPilot kullanıcı gizliliğine tavizsiz bir bağlılıkla tasarlanmıştır:

- 🚫 **Sıfır Telemetri:** Firebase Analytics, Sentry, uzaktan çökme raporlayıcıları veya takip SDK'ları yer almaz.
- 📵 **Bulut Eşitlemesi Yok:** Kurallarınız, günlükleriniz ve anahtarlarınız asla üçüncü taraf bir buluta gönderilmez.
- 🛡️ **Donanım Destekli Keystore:** Webhook şifreleri ve özel başlıklar Android Keystore donanım anahtarlarıyla AES-256-GCM ile korunur.
- 🛡️ **Android Keystore Koruması:** Webhook şifreleri ve özel başlıklar AES-256-GCM ile Android Keystore içinde, cihaz desteklediğinde donanım destekli olarak korunur.
- 🙈 **Kişisel Veri Maskeleme:** Telefon numaraları, webhook anahtarları ve gizli başlıklar arayüzde ve loglarda maskelenmiş olarak tutulur.

### Şeffaf İzin Açıklamaları
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ android {
applicationId = "com.flowpilot.app"
minSdk = 26
targetSdk = 36
versionCode = 3
versionName = "1.0.2"
versionCode = 4
versionName = "1.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down
Loading
Loading