From 618d26bc7f5a99d5600d4401d35c237514a70a76 Mon Sep 17 00:00:00 2001 From: ADSFAaron Date: Sun, 30 Aug 2026 21:38:49 +0800 Subject: [PATCH 1/3] docs(play): release notes for 1.5.0 in all four store languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Play caps "What's new" at 500 characters per language and rejects rather than truncates, so the counts are recorded next to each block and the file verifies them from its own fenced blocks. Written for users, not as a second CHANGELOG. The schedule permission leads because it is the only item here that needs the user to do something — every other fix takes effect on install. Co-Authored-By: Claude Opus 5 --- docs/play-release-notes.md | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/play-release-notes.md diff --git a/docs/play-release-notes.md b/docs/play-release-notes.md new file mode 100644 index 0000000..71a6d38 --- /dev/null +++ b/docs/play-release-notes.md @@ -0,0 +1,74 @@ +# Play 更新資訊(What's new)— 1.5.0 + +路徑:Play 管理中心 → 發布 → 正式版 → 建立新版本 → 版本資訊,切換到各語言分頁。 + +- **上限每種語言 500 字元**(中日文一個字算一個字元)。超過會被擋下,不會自動截斷。 +- 語言代碼要與商店資訊已啟用的語言一致:`zh-TW` / `en-US` / `ja-JP` / `zh-CN`。 +- 這是寫給使用者看的,不是 CHANGELOG。挑「使用者看得到、或需要使用者動手」的事就好; + 完整的技術變更留在 [CHANGELOG.md](../CHANGELOG.md)。 +- 這一版最該講的是**排程權限**:那是唯一需要使用者自己去授權才會好的項目, + 其餘都是安裝完就生效。 + +--- + +## 繁體中文(zh-TW)— 223 字元 + +``` +【看得見正在執行的流程】 +流程執行中,卡片右下角會顯示「執行中」。不論是側滑執行、時間到,或是背景觸發,都看得到。通知會寫出正在執行哪一個流程;長按右上角的閃電,可列出目前所有執行中的流程。 + +【排程準時了】 +排定 9:00 卻晚十分鐘才跑,是因為缺少「鬧鐘和提醒」權限。流程卡片現在會提示並帶你前往授權,授權後既有排程立即重新校正。 + +【修正】 +• App 開著時不再獨占 NFC,感應網址標籤或交通卡恢復正常 +• 音量動作的摘要不再顯示「?」 +``` + +--- + +## English(en-US)— 497 字元 + +``` +SEE WHAT'S RUNNING +A running flow now shows "Running" on its card — whether you swiped it, a schedule fired it, or a background trigger did. The notification names the flow, and a long press on the lightning bolt lists them all. + +SCHEDULES RUN ON TIME +A flow set for 9:00 could run ten minutes late: the "Alarms & reminders" permission was never requested. Flows now flag it and take you there. + +FIXED +• NFC is no longer taken over while the app is open +• Volume actions no longer summarise as "?" +``` + +--- + +## 日本語(ja-JP)— 275 字元 + +``` +【実行中のフローが見えます】 +フローの実行中、カード右下に「実行中」と表示されます。スワイプ実行でも、時刻やバックグラウンドのトリガーでも同じです。通知には実行中のフロー名が表示され、右上の稲妻を長押しすると実行中のフローが一覧できます。 + +【スケジュールが時間どおりに】 +9:00 に設定したフローが10分遅れることがありました。「アラームとリマインダー」の権限を要求していなかったためです。不足時はカードに表示し、設定画面へ案内します。 + +【修正】 +・アプリ起動中に NFC を占有しなくなりました +・音量アクションの概要が「?」にならないように +``` + +--- + +## 简体中文(zh-CN)— 223 字元 + +``` +【看得见正在运行的流程】 +流程运行中,卡片右下角会显示「运行中」。无论是侧滑运行、时间到,还是后台触发,都看得到。通知会写出正在运行哪一个流程;长按右上角的闪电,可列出当前所有运行中的流程。 + +【定时准时了】 +设定 9:00 却晚十分钟才跑,是因为缺少「闹钟和提醒」权限。流程卡片现在会提示并带你前往授权,授权后既有定时立即重新校正。 + +【修正】 +• App 开着时不再独占 NFC,感应网址标签或交通卡恢复正常 +• 音量动作的摘要不再显示「?」 +``` From 897abb1450e0bc1daf4681ec3590c6de8b4b5f85 Mon Sep 17 00:00:00 2001 From: ADSFAaron Date: Sun, 30 Aug 2026 22:07:24 +0800 Subject: [PATCH 2/3] feat(nfc): background tag dispatch, for the users who actually have an NFC flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.5.0 removed the manifest TAG_DISCOVERED filter because it registered every install as the handler for any tag no other app claimed — door badges and transit cards launched NexFlow for people who had never built an NFC flow. That fixed the hijacking and silently took background NFC with it: with the app closed, a tag now does nothing at all. The filter did not need to be permanent. It lives on an activity-alias that ships disabled, and NfcBackgroundDispatch turns it on only while an enabled flow has an NFC trigger. A disabled component is excluded from intent resolution outright, so a user with no NFC flow does not rank low in tag dispatch — they are not in it. Both directions are written: enabled state survives reboots and updates, so leaving it on after the last NFC flow is deleted would keep intercepting tags for a feature the user removed. The engine hands dispatch back when it stops, rather than swallowing taps that would find nothing listening. TECH_DISCOVERED rather than TAG_DISCOVERED, which API 37 deprecates. The tech-list is Ndef and NdefFormatable and nothing else. Ndef is the one technology the platform guarantees ("mandatory for all Android devices with NFC to correctly enumerate"), so the list matches the same tags on every phone; MifareClassic and MifareUltralight are explicitly optional and would have matched nothing at all on a non-NXP chipset, with no error and no log. It is also a tighter net than what it replaces: transit passes and badges are MifareClassic, payment cards are IsoDep, Suica is NfcF, and none of them enumerate Ndef. A tag opens a transparent activity that hands the id over and finishes — nothing appears over what the user was doing. NfcEventSource now holds a tag that arrives before anything is listening: a tag on a dead process starts the service and emits in the same breath, and with replay = 0 that emit landed nowhere, so the tag woke the app and then nothing happened. The alias has no Kotlin class and is addressed by a string, so a manifest rename cannot fail at compile time. It is logged rather than swallowed, and the test resolves the name against the merged manifest. Verified on device that a fresh install appears in neither TECH nor TAG dispatch. The enabled half is not verifiable here — shell cannot toggle another app's components, and there is no tag to scan — so a real tap with a real tag is still worth doing before release. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 19 +++ CLAUDE.md | 4 +- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 28 ++++ .../java/com/nexflow/event/NfcEventSource.kt | 30 ++++- .../com/nexflow/nfc/NfcBackgroundDispatch.kt | 108 +++++++++++++++ .../java/com/nexflow/nfc/NfcTagRunActivity.kt | 69 ++++++++++ .../java/com/nexflow/service/FlowEngine.kt | 12 +- app/src/main/res/xml/nfc_tech_filter.xml | 33 +++++ .../com/nexflow/event/NfcEventSourceTest.kt | 92 +++++++++++++ .../nexflow/nfc/NfcBackgroundDispatchTest.kt | 126 ++++++++++++++++++ .../service/FlowEngineConditionTest.kt | 2 + .../service/FlowEngineRunningStateTest.kt | 2 + .../service/FlowEngineTriggerIsolationTest.kt | 2 + docs/play-release-notes.md | 66 ++++++++- 15 files changed, 588 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/nexflow/nfc/NfcBackgroundDispatch.kt create mode 100644 app/src/main/java/com/nexflow/nfc/NfcTagRunActivity.kt create mode 100644 app/src/main/res/xml/nfc_tech_filter.xml create mode 100644 app/src/test/java/com/nexflow/event/NfcEventSourceTest.kt create mode 100644 app/src/test/java/com/nexflow/nfc/NfcBackgroundDispatchTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ade7a..fb91157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ (尚無項目) +## [1.5.1] - 2026-08-30 + +把 1.5.0 為了修好「NFC 被獨占」而一併拿掉的背景感應補回來,但這次只發給真的需要的人。 + +### 新增(Added) + +- **關著 App 也能感應 NFC 標籤觸發流程**。1.5.0 移除了 manifest 的 `TAG_DISCOVERED` 攔截——它是「所有其他 App 都不處理的標籤我全收」,門禁卡、交通卡因此都會把 NexFlow 叫起來。代價是背景感應一起沒了。現在改成 `` 承載攔截、預設關閉,只在「有啟用中的 NFC 流程」時才由程式打開: + - **沒有 NFC 流程的使用者,NexFlow 完全不會出現在標籤分派名單裡**。被停用的元件不參與 intent 解析,所以不是「排在後面」,是根本不在 + - 感應後開啟的是一個透明、無介面、跑完即關的 Activity,畫面上不會彈出任何東西 + - 改用 `TECH_DISCOVERED` 而非已被 API 37 標為 deprecated 的 `TAG_DISCOVERED`。tech-list 只列 `Ndef` 與 `NdefFormatable`:`Ndef` 是官方文件唯一保證「所有支援 NFC 的 Android 裝置都必須正確列舉」的技術,而 `MifareClassic`/`MifareUltralight` 是選配的——列了它們會在非 NXP 晶片的手機上安靜地永遠不匹配。這份清單同時把抱怨來源擋在外面:交通卡與門禁卡是 MifareClassic、感應支付是 IsoDep、Suica 是 NfcF,都不會列舉 `Ndef` + - 引擎停止時交還標籤分派。自動化關掉時攔下感應卻什麼都不做,等於白白吃掉使用者的一次感應 +- 標籤在引擎訂閱之前抵達時會先保留再送出。標籤喚醒的是一個已經死掉的程序,`NfcEventSource` 原本 `replay = 0`,那一次 emit 會落空——結果是 App 被叫醒、流程沒跑 + +### 已知限制 + +- 寫入網址的標籤仍然會開啟瀏覽器:`NDEF_DISCOVERED` 的優先權在 `TECH_DISCOVERED` 之上,這是對的。空白或只用 UID 的標籤(NexFlow NFC 觸發器的實際用法)不受影響 +- 非 NDEF 標籤(門禁卡、交通卡當觸發器)在背景不會觸發。App 開著時 reader mode 仍然讀得到所有標籤 + ## [1.5.0] - 2026-08-30 這一版處理的是「我怎麼知道它有沒有在動」,以及兩個安靜到不像故障的故障。執行中的流程現在會在卡片、通知與長按閃電的清單裡同時現身;排程晚到十分鐘的原因是一個從來沒被要求過的權限;而 NexFlow 只要開著,就會把整支手機的 NFC 感應全部吃掉。 @@ -249,6 +267,7 @@ AI 對話從「能用」變成「好用」:回覆逐字出現而不是等半 - 多語言:繁體中文、簡體中文、日文、英文 - 匯入/匯出 `.flow`(JSON)與 MacroDroid `.mdr` 相容解析 +[1.5.1]: https://github.com/ADSFAaron/NexFlow/releases/tag/v1.5.1 [1.5.0]: https://github.com/ADSFAaron/NexFlow/releases/tag/v1.5.0 [1.4.0]: https://github.com/ADSFAaron/NexFlow/releases/tag/v1.4.0 [1.3.0]: https://github.com/ADSFAaron/NexFlow/releases/tag/v1.3.0 diff --git a/CLAUDE.md b/CLAUDE.md index c3de9a7..323efa3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,9 @@ core/macrodroid-compat/ # MacroDroid .mdr 格式解析 | `AccessibilityService.dispatchGesture()` | 沒在 accessibility-service XML 宣告 `android:canPerformGestures="true"` 就只會回 `false`,不丟例外、不寫 log,模擬點擊/滑動整個靜默失效 | `nexflow_accessibility_config.xml` 一定要有 `android:canPerformGestures="true"` | | `GestureDescription.StrokeDescription` | 座標超出螢幕範圍會丟 `IllegalArgumentException`;座標來自使用者手打的設定,在 service 的 coroutine 裡丟出去會拖垮整個無障礙服務 | 建 `StrokeDescription` 要包 `runCatching`,失敗就回報 action 失敗。單點 `moveTo`(不 `lineTo`)是合法的「不移動的觸碰」,可直接當點擊用 | | `NfcAdapter.enableReaderMode()` | **獨占且全裝置生效**:開著的時候所有感應都只進自己的 callback,別的 App 一律看不到標籤;`FLAG_READER_SKIP_NDEF_CHECK` 更直接關掉 NDEF 分派(javadoc 原話「NDEF-based tag dispatch will not be functional」)。在 `onResume` 無條件開啟=只要 App 開著就吃掉使用者的網址標籤與交通卡 | 只在真的要用時開(有啟用中的 NFC 流程、或正在掃描設定),且只能有一個持有者。設定頁想掃卡就向那個持有者提出請求,不要自己 `enableReaderMode`——第二個持有者關閉時會把第一個的也關掉 | -| manifest `android.nfc.action.TAG_DISCOVERED` | 分派順序是 NDEF → TECH → TAG,TAG 是最後手段。若機器上只有你註冊,就成了所有其他 App 不處理的標籤(門禁卡、交通卡)的萬用接收者。API 37 已標 `@Deprecated` | 前景 NFC 用 reader mode 就夠,不要註冊這個 filter。真要背景 NFC 才用 `TECH_DISCOVERED` + 明確 tech-list | +| manifest `android.nfc.action.TAG_DISCOVERED` | 分派順序是 NDEF → TECH → TAG,TAG 是最後手段。若機器上只有你註冊,就成了所有其他 App 不處理的標籤(門禁卡、交通卡)的萬用接收者。API 37 已標 `@Deprecated` | 背景 NFC 用 `TECH_DISCOVERED` + 明確 tech-list,並把 filter 放在預設 `enabled="false"` 的 `` 上,用 `setComponentEnabledSetting` 只在真的有 NFC 流程時打開——被停用的元件不參與 intent 解析。見 `NfcBackgroundDispatch` | +| NFC `tech-list` 匹配語意 | **反直覺**:官方定義是「tech-list 是標籤所報技術的 subset 才算匹配」,所以**列愈少技術匹配愈廣**。同一個 `` 內是 AND,多個 `` 之間是 OR。很多人以為列一個就是「只收這種」,其實是「所有帶這種的都收」 | 想收窄就多列幾個技術;想分開涵蓋就開多個 `` | +| `android.nfc.tech.Mifare*` 放進 tech-list | `MifareClassic`/`MifareUltralight` 的實作在 Android 上是**選配**的(javadoc:「If it is not implemented, then MifareClassic will never be enumerated in `getTechList`」),非 NXP 晶片的手機上永遠不會出現 → tech-list 安靜地永不匹配,不報錯也沒 log | tech-list 只用 `Ndef`/`NdefFormatable`/`NfcA`~`NfcV`。`Ndef` 是唯一「所有支援 NFC 的裝置都必須正確列舉」的,跨機一致 | | `SCHEDULE_EXACT_ALARM` | targetSdk 33+ 的 App **全新安裝時預設拒絕**(備份還原到新機也是拒絕),但從舊版升級的裝置會保留已授予的 → 同一版有人正常有人遲到。沒授權時退回不精確鬧鐘,官方保證只有「一小時內」,排 9:00 的流程 9:10 才跑 | 宣告不等於拿到:一定要 `canScheduleExactAlarms()` 檢查 + 用 `ACTION_REQUEST_SCHEDULE_EXACT_ALARM` 引導。並且要收 `ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED` 重排——鬧鐘的精度是排定當下決定的,授權後既有鬧鐘不會自己升級 | ### 3. 測試 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 611cd21..a2ed2f0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,8 +28,8 @@ android { applicationId = "com.adsf.nexflow" minSdk = 30 targetSdk = 37 - versionCode = 9 - versionName = "1.5.0" + versionCode = 10 + versionName = "1.5.1" testInstrumentationRunner = "com.nexflow.HiltTestRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5fdd45d..cb6dfa9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -155,6 +155,34 @@ + + + + + + + + + + + ( @@ -29,10 +34,27 @@ object NfcEventSource { extraBufferCapacity = 8, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - val events = _events.asSharedFlow() + + /** + * A tag scanned before anything was listening. A tag arriving on a dead process starts the + * service and emits in the same breath, and the engine cannot have subscribed yet — with + * `replay = 0` that emit lands nowhere, so the tag would wake the app and then do nothing. + * Held here instead and delivered to the first subscriber. + */ + private val pending = AtomicReference(null) + + val events: Flow = _events.asSharedFlow() + .onSubscription { pending.getAndSet(null)?.let { emit(it) } } /** @param tagId hex string of the NFC tag UID, e.g. "04A3B2C1" */ fun emit(tagId: String) { - _events.tryEmit(tagId) + // Checked rather than always buffered: with a live collector the tag must go straight + // through, and stashing it as well would fire the flow twice. + if (_events.subscriptionCount.value == 0) pending.set(tagId) else _events.tryEmit(tagId) + } + + /** Drops a tag held for a subscriber that never came. */ + fun clearPending() { + pending.set(null) } } diff --git a/app/src/main/java/com/nexflow/nfc/NfcBackgroundDispatch.kt b/app/src/main/java/com/nexflow/nfc/NfcBackgroundDispatch.kt new file mode 100644 index 0000000..e387c93 --- /dev/null +++ b/app/src/main/java/com/nexflow/nfc/NfcBackgroundDispatch.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2026 NexFlow Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nexflow.nfc + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import com.nexflow.core.automation.model.Flow +import com.nexflow.core.automation.model.TriggerType +import com.nexflow.event.NfcEventSource +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Registers NexFlow for NFC tag dispatch only while a flow could actually use a tag. + * + * A manifest intent filter is normally all-or-nothing, and that is what made the previous + * `TAG_DISCOVERED` filter a problem: every user was permanently registered as the handler for + * every tag no other app claimed, whether or not they had ever built an NFC flow. An + * `` shipped disabled and toggled here is the way to make the filter + * conditional — a disabled component is excluded from intent resolution entirely, so NexFlow + * does not appear in tag dispatch at all until [sync] turns it on. + * + * Enabled state survives reboots and updates, so both directions have to be written: leaving it + * on after the last NFC flow is deleted would keep intercepting tags for a feature the user no + * longer has. + */ +@Singleton +class NfcBackgroundDispatch @Inject constructor( + @param:ApplicationContext private val context: Context, +) { + private val alias = ComponentName(context, ALIAS_CLASS) + + /** Enable dispatch when any enabled flow has an NFC trigger, disable it otherwise. */ + fun sync(flows: List) { + val wanted = flows.any { flow -> + flow.enabled && flow.triggers.any { it.type == TriggerType.NFC_TAG } + } + setEnabled(wanted) + } + + /** + * Give up tag dispatch. Called when the engine stops: with automation off a tag would wake + * the app, find nothing listening and do nothing visible — an intercepted tap that costs the + * user their tag read and returns nothing. + */ + fun disable() { + setEnabled(false) + // Nothing will collect it now, and a stale id delivered whenever the engine next starts + // would fire a flow the user triggered minutes or days ago. + NfcEventSource.clearPending() + } + + private fun setEnabled(enabled: Boolean) { + val target = if (enabled) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + // Read first: this is a cross-process call that writes to package state, and sync() runs + // on every change to the flow list. + if (runCatching { context.packageManager.getComponentEnabledSetting(alias) } + .getOrNull() == target + ) { + return + } + try { + context.packageManager.setComponentEnabledSetting( + alias, + target, + // Without DONT_KILL_APP the platform restarts the process to apply this — which, + // called from the running engine, would kill the service mid-flow. + PackageManager.DONT_KILL_APP, + ) + } catch (e: IllegalArgumentException) { + // The alias is addressed by a string, so a rename in the manifest lands here rather + // than at compile time. Swallowing it silently would mean background NFC simply + // never works, with nothing to show why. NfcBackgroundDispatchTest asserts the name + // resolves against the merged manifest so this should be unreachable. + Log.e(TAG, "NFC dispatch alias $ALIAS_CLASS is not declared in the manifest", e) + } + } + + private companion object { + const val TAG = "NfcBackgroundDispatch" + + /** + * The alias is manifest-only (no Kotlin class of its own), so it is addressed by name. + * Must match the `android:name` of the activity-alias in AndroidManifest.xml. + */ + const val ALIAS_CLASS = "com.nexflow.nfc.NfcTagDispatchAlias" + } +} diff --git a/app/src/main/java/com/nexflow/nfc/NfcTagRunActivity.kt b/app/src/main/java/com/nexflow/nfc/NfcTagRunActivity.kt new file mode 100644 index 0000000..a04f0f2 --- /dev/null +++ b/app/src/main/java/com/nexflow/nfc/NfcTagRunActivity.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2026 NexFlow Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nexflow.nfc + +import android.content.Intent +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.core.content.IntentCompat +import com.nexflow.event.NfcEventSource +import com.nexflow.prefs.ServiceEnabledPrefs +import com.nexflow.service.FlowExecutionService + +/** + * What a tag opens when NexFlow is closed. It shows nothing and finishes immediately — the user + * taps their tag and the flow runs, with no app appearing over whatever they were doing. + * + * An Activity is the only way in: the tag dispatch system starts activities, there is no + * broadcast for it. Reaching it at all requires [NfcTagDispatchAlias][com.nexflow.nfc] to be + * enabled, which only happens while a flow that could use a tag exists — see + * [NfcBackgroundDispatch]. + * + * Unlike [com.nexflow.shortcut.ShortcutRunActivity] this does not stay alive for the run. A + * shortcut has to host a SHOW_MENU sheet over the launcher; a tag hands its id to the engine + * and is done, and the engine owns everything after that. + */ +class NfcTagRunActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + dispatch(intent) + finish() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + dispatch(intent) + finish() + } + + private fun dispatch(intent: Intent?) { + intent ?: return + val tag = IntentCompat.getParcelableExtra(intent, NfcAdapter.EXTRA_TAG, Tag::class.java) + ?: return + val tagId = tag.id.joinToString("") { "%02X".format(it) } + + // The tag may have woken a dead process, so the engine that listens for it might not be + // running yet. Start it first; NfcEventSource holds the id until the engine subscribes. + // Respect the master switch: if the user turned automation off, a tag must not turn it + // back on, and there is then nothing to hand the id to. + if (!ServiceEnabledPrefs.get(this)) return + NfcEventSource.emit(tagId) + FlowExecutionService.start(this) + } +} diff --git a/app/src/main/java/com/nexflow/service/FlowEngine.kt b/app/src/main/java/com/nexflow/service/FlowEngine.kt index 9951dfe..7b67e50 100644 --- a/app/src/main/java/com/nexflow/service/FlowEngine.kt +++ b/app/src/main/java/com/nexflow/service/FlowEngine.kt @@ -35,6 +35,7 @@ import com.nexflow.core.automation.repository.GlobalVariableRepository import com.nexflow.core.automation.trigger.TriggerEvent import com.nexflow.core.automation.trigger.TriggerHandler import com.nexflow.core.automation.trigger.TriggerVariables +import com.nexflow.nfc.NfcBackgroundDispatch import com.nexflow.prefs.DetailedLogPrefs import com.nexflow.prefs.ExecutionFeedbackPrefs import com.nexflow.trigger.TimeTriggerScheduler @@ -67,6 +68,7 @@ class FlowEngine @Inject constructor( private val actionExecutorSet: Set<@JvmSuppressWildcards ActionExecutor>, private val conditionEvaluatorSet: Set<@JvmSuppressWildcards ConditionEvaluator>, private val timeTriggerScheduler: TimeTriggerScheduler, + private val nfcBackgroundDispatch: NfcBackgroundDispatch, @param:ApplicationContext private val context: Context, ) { private companion object { @@ -97,7 +99,12 @@ class FlowEngine @Inject constructor( // in-process streams below, so they survive Doze and service death. Keep alarms in // sync with the current enabled set on every change. launch { - repository.observeEnabled().collect { timeTriggerScheduler.sync(it) } + repository.observeEnabled().collect { + timeTriggerScheduler.sync(it) + // Same idea, different mechanism: claim NFC tag dispatch only while a flow + // could use a tag, so a user with no NFC flow never intercepts one. + nfcBackgroundDispatch.sync(it) + } } // Rebuild the in-process trigger streams ONLY when the trigger structure changes. @@ -165,6 +172,9 @@ class FlowEngine @Inject constructor( fun stop() { engineJob?.cancel() engineJob = null + // Automation is off, so a tag has nothing to start. Hand dispatch back rather than + // swallowing taps that would do nothing. + nfcBackgroundDispatch.disable() // Half-finished ALL combinations belong to the session that started them. allTriggersGate.clear() } diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 0000000..5c884f0 --- /dev/null +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,33 @@ + + + + + android.nfc.tech.Ndef + + + android.nfc.tech.NdefFormatable + + diff --git a/app/src/test/java/com/nexflow/event/NfcEventSourceTest.kt b/app/src/test/java/com/nexflow/event/NfcEventSourceTest.kt new file mode 100644 index 0000000..03797bb --- /dev/null +++ b/app/src/test/java/com/nexflow/event/NfcEventSourceTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026 NexFlow Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nexflow.event + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +/** + * A tag that wakes a dead process arrives before the engine exists to hear it. The activity + * starts the service and emits in the same breath, and with `replay = 0` that emit lands + * nowhere — the tag would wake the app and then do nothing, which is the exact failure the + * background-NFC feature exists to avoid. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class NfcEventSourceTest { + + @BeforeEach + @AfterEach + fun clean() = NfcEventSource.clearPending() + + @Test + fun `a tag scanned before anything is listening reaches the first subscriber`() = runTest { + NfcEventSource.emit("04A3B2C1") + + assertEquals("04A3B2C1", NfcEventSource.events.first()) + } + + @Test + fun `a held tag is delivered once, not to every later subscriber`() = runTest { + NfcEventSource.emit("04A3B2C1") + assertEquals("04A3B2C1", NfcEventSource.events.first()) + + // A second subscriber (the engine re-subscribing after a flow edit) must not re-run the + // flow with a tag the user tapped once, minutes ago. + val second = launch { NfcEventSource.events.first() } + yield() + assertEquals(true, second.isActive, "the tag must not be replayed to a new subscriber") + second.cancel() + } + + @Test + fun `a tag scanned while the engine is listening goes straight through`() = runTest { + val received = mutableListOf() + val job = launch { NfcEventSource.events.take(1).toList(received) } + yield() + + NfcEventSource.emit("DEADBEEF") + job.join() + + assertEquals(listOf("DEADBEEF"), received) + // It must not ALSO have been stashed, or the next subscriber would fire the flow again. + val later = launch { NfcEventSource.events.first() } + yield() + assertEquals(true, later.isActive) + later.cancel() + } + + @Test + fun `a tag held for a subscriber that never came can be dropped`() = runTest { + NfcEventSource.emit("04A3B2C1") + // Automation was switched off before the engine started: delivering this whenever the + // engine next comes up would fire a flow the user triggered days ago. + NfcEventSource.clearPending() + + val waiting = launch { NfcEventSource.events.first() } + yield() + assertEquals(true, waiting.isActive) + waiting.cancel() + } +} diff --git a/app/src/test/java/com/nexflow/nfc/NfcBackgroundDispatchTest.kt b/app/src/test/java/com/nexflow/nfc/NfcBackgroundDispatchTest.kt new file mode 100644 index 0000000..2d1b3f4 --- /dev/null +++ b/app/src/test/java/com/nexflow/nfc/NfcBackgroundDispatchTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2026 NexFlow Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nexflow.nfc + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nexflow.core.automation.model.Flow +import com.nexflow.core.automation.model.Trigger +import com.nexflow.core.automation.model.TriggerLogic +import com.nexflow.core.automation.model.TriggerType +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Which users are registered for NFC tag dispatch. + * + * This is the whole point of the alias: a plain manifest filter registered *every* install as a + * handler for tags no other app claimed — door badges and transit cards included — regardless of + * whether the user had ever built an NFC flow. Getting the enable condition wrong in either + * direction brings that back, so both directions are pinned here. + * + * Robolectric because the component's enabled state lives in PackageManager. + */ +@RunWith(AndroidJUnit4::class) +class NfcBackgroundDispatchTest { + + private val context = ApplicationProvider.getApplicationContext() + private val dispatch = NfcBackgroundDispatch(context) + private val alias = ComponentName(context, "com.nexflow.nfc.NfcTagDispatchAlias") + + private fun state(): Int = context.packageManager.getComponentEnabledSetting(alias) + + private fun flow(enabled: Boolean, type: TriggerType) = Flow( + id = "f-${type.name}-$enabled", + schemaVersion = 1, + name = "test", + description = "", + author = null, + tags = emptyList(), + enabled = enabled, + createdAt = 0L, + updatedAt = 0L, + triggers = listOf(Trigger(id = "t1", type = type, config = emptyMap())), + triggerLogic = TriggerLogic.ANY, + conditions = emptyList(), + actions = emptyList(), + variables = emptyList(), + ) + + @Test + fun `the alias this toggles actually exists in the manifest`() { + // The alias has no Kotlin class, so it is addressed by a string and a rename in the + // manifest cannot fail at compile time — it would just mean background NFC silently + // never works. MATCH_DISABLED_COMPONENTS because the alias ships disabled. + val info = context.packageManager.getActivityInfo( + alias, + PackageManager.MATCH_DISABLED_COMPONENTS, + ) + + assertEquals("com.nexflow.nfc.NfcTagRunActivity", info.targetActivity) + // Shipping it enabled would put every install back in tag dispatch, which is the bug. + assertEquals(false, info.enabled) + } + + @Test + fun `an enabled NFC flow claims tag dispatch`() { + dispatch.sync(listOf(flow(enabled = true, type = TriggerType.NFC_TAG))) + + assertEquals(PackageManager.COMPONENT_ENABLED_STATE_ENABLED, state()) + } + + @Test + fun `a user with no NFC flow never appears in tag dispatch`() { + dispatch.sync(listOf(flow(enabled = true, type = TriggerType.TIME))) + + // Not merely "not enabled": the alias ships disabled, and this must leave it that way. + assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DISABLED, state()) + } + + @Test + fun `a disabled NFC flow does not claim tag dispatch`() { + dispatch.sync(listOf(flow(enabled = false, type = TriggerType.NFC_TAG))) + + assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DISABLED, state()) + } + + @Test + fun `deleting the last NFC flow gives tag dispatch back`() { + dispatch.sync(listOf(flow(enabled = true, type = TriggerType.NFC_TAG))) + assertEquals(PackageManager.COMPONENT_ENABLED_STATE_ENABLED, state()) + + // The enabled state is persisted by the platform, so it survives reboots and updates — + // leaving it on would keep intercepting tags for a feature the user has removed. + dispatch.sync(emptyList()) + + assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DISABLED, state()) + } + + @Test + fun `stopping the engine gives tag dispatch back`() { + dispatch.sync(listOf(flow(enabled = true, type = TriggerType.NFC_TAG))) + + // Automation off: a tag would wake the app, find nothing listening and do nothing — + // costing the user the tag read and returning nothing for it. + dispatch.disable() + + assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DISABLED, state()) + } +} diff --git a/app/src/test/java/com/nexflow/service/FlowEngineConditionTest.kt b/app/src/test/java/com/nexflow/service/FlowEngineConditionTest.kt index 152b66a..3bf9cf2 100644 --- a/app/src/test/java/com/nexflow/service/FlowEngineConditionTest.kt +++ b/app/src/test/java/com/nexflow/service/FlowEngineConditionTest.kt @@ -32,6 +32,7 @@ import com.nexflow.core.automation.model.TriggerLogic import com.nexflow.core.automation.model.TriggerType import com.nexflow.core.automation.repository.FlowRepository import com.nexflow.core.automation.repository.GlobalVariableRepository +import com.nexflow.nfc.NfcBackgroundDispatch import com.nexflow.core.automation.trigger.TriggerVariables import com.nexflow.trigger.TimeTriggerScheduler import io.mockk.coEvery @@ -103,6 +104,7 @@ class FlowEngineConditionTest { actionExecutorSet = setOf(executor), conditionEvaluatorSet = evaluators, timeTriggerScheduler = mockk(relaxed = true), + nfcBackgroundDispatch = mockk(relaxed = true), context = context, ) } diff --git a/app/src/test/java/com/nexflow/service/FlowEngineRunningStateTest.kt b/app/src/test/java/com/nexflow/service/FlowEngineRunningStateTest.kt index 33ec635..ca1e936 100644 --- a/app/src/test/java/com/nexflow/service/FlowEngineRunningStateTest.kt +++ b/app/src/test/java/com/nexflow/service/FlowEngineRunningStateTest.kt @@ -24,6 +24,7 @@ import com.nexflow.core.automation.model.Flow import com.nexflow.core.automation.model.TriggerLogic import com.nexflow.core.automation.repository.FlowRepository import com.nexflow.core.automation.repository.GlobalVariableRepository +import com.nexflow.nfc.NfcBackgroundDispatch import com.nexflow.trigger.TimeTriggerScheduler import io.mockk.coEvery import io.mockk.every @@ -91,6 +92,7 @@ class FlowEngineRunningStateTest { actionExecutorSet = setOf(executor), conditionEvaluatorSet = emptySet(), timeTriggerScheduler = mockk(relaxed = true), + nfcBackgroundDispatch = mockk(relaxed = true), context = context, ) } diff --git a/app/src/test/java/com/nexflow/service/FlowEngineTriggerIsolationTest.kt b/app/src/test/java/com/nexflow/service/FlowEngineTriggerIsolationTest.kt index ca726b8..fa83496 100644 --- a/app/src/test/java/com/nexflow/service/FlowEngineTriggerIsolationTest.kt +++ b/app/src/test/java/com/nexflow/service/FlowEngineTriggerIsolationTest.kt @@ -23,6 +23,7 @@ import com.nexflow.core.automation.model.TriggerType import com.nexflow.core.automation.repository.FlowRepository import com.nexflow.core.automation.trigger.TriggerEvent import com.nexflow.core.automation.trigger.TriggerHandler +import com.nexflow.nfc.NfcBackgroundDispatch import com.nexflow.trigger.TimeTriggerScheduler import io.mockk.coEvery import io.mockk.coVerify @@ -106,6 +107,7 @@ class FlowEngineTriggerIsolationTest { actionExecutorSet = emptySet(), conditionEvaluatorSet = emptySet(), timeTriggerScheduler = scheduler, + nfcBackgroundDispatch = mockk(relaxed = true), context = context, ) diff --git a/docs/play-release-notes.md b/docs/play-release-notes.md index 71a6d38..dbb9526 100644 --- a/docs/play-release-notes.md +++ b/docs/play-release-notes.md @@ -1,4 +1,4 @@ -# Play 更新資訊(What's new)— 1.5.0 +# Play 更新資訊(What's new) 路徑:Play 管理中心 → 發布 → 正式版 → 建立新版本 → 版本資訊,切換到各語言分頁。 @@ -11,6 +11,8 @@ --- +# 1.5.0 + ## 繁體中文(zh-TW)— 223 字元 ``` @@ -72,3 +74,65 @@ FIXED • App 开着时不再独占 NFC,感应网址标签或交通卡恢复正常 • 音量动作的摘要不再显示「?」 ``` + +--- + +# 1.5.1 + +## 繁體中文(zh-TW)— 231 字元 + +``` +【關著 App 也能用 NFC 標籤觸發流程】 +1.5.0 為了不讓 NexFlow 攔截門禁卡、交通卡,拿掉了背景 NFC。現在補回來,但只發給真的需要的人:只有在你有「啟用中的 NFC 流程」時,NexFlow 才會登記接收標籤;沒有的話完全不會出現在感應名單裡。 + +感應後不會跳出任何畫面,流程直接在背景執行。 + +【說明】 +• 寫了網址的標籤仍會開啟瀏覽器,這是系統的優先順序 +• 交通卡、門禁卡這類非 NDEF 標籤,背景不會觸發;App 開著時仍讀得到 +``` + +--- + +## English(en-US)— 470 字元 + +``` +NFC TAGS WORK WITH THE APP CLOSED +1.5.0 dropped background NFC so NexFlow would stop intercepting door badges and transit cards. It's back, but only for people who need it: NexFlow registers for tags only while you have an enabled NFC flow, and otherwise never appears in tag handling at all. + +Scanning shows nothing on screen — the flow just runs. + +NOTES +• Tags carrying a URL still open the browser +• Non-NDEF tags such as transit cards won't trigger in the background +``` + +--- + +## 日本語(ja-JP)— 270 字元 + +``` +【アプリを閉じていても NFC タグでフローが動きます】 +1.5.0 では、社員証や交通系 IC カードを横取りしないよう、バックグラウンドの NFC を外していました。今回、必要な人にだけ戻します。有効な NFC フローがあるときだけタグの受け取りに登録し、なければ一覧に一切現れません。 + +タグをかざしても画面には何も出ず、フローだけが実行されます。 + +【注意】 +・URL が書かれたタグはブラウザが開きます(システムの優先順位) +・交通系 IC など NDEF でないタグはバックグラウンドでは動きません。アプリを開いていれば読めます +``` + +--- + +## 简体中文(zh-CN)— 231 字元 + +``` +【关着 App 也能用 NFC 标签触发流程】 +1.5.0 为了不让 NexFlow 拦截门禁卡、交通卡,去掉了后台 NFC。现在补回来,但只发给真的需要的人:只有在你有「启用中的 NFC 流程」时,NexFlow 才会登记接收标签;没有的话完全不会出现在感应名单里。 + +感应后不会弹出任何画面,流程直接在后台执行。 + +【说明】 +• 写了网址的标签仍会打开浏览器,这是系统的优先顺序 +• 交通卡、门禁卡这类非 NDEF 标签,后台不会触发;App 开着时仍读得到 +``` From f2aa5ddcff0a90f6b81493210406af62b2d77fbb Mon Sep 17 00:00:00 2001 From: ADSFAaron Date: Mon, 31 Aug 2026 12:25:56 +0800 Subject: [PATCH 3/3] docs: Play copy for the 1.5.1 release, and correct what 1.5.1 makes false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.5.0 was never published, so the store's users go from 1.4.0 straight to 1.5.1 and the "What's new" has to cover both versions rather than only the NFC work. Rewritten as one combined set, with the note in CHANGELOG so nobody later reads the 1.5.0 entry as something that shipped. English loses the volume-summary line to the 500-character cap — it is the least dense of the four languages and that is the smallest of the four items. README claimed "NFC 觸發僅在 App 前景時有效" in two places, which 1.5.1 makes wrong, and understated the alarm fallback as "誤差數分鐘" when the documented guarantee for an inexact alarm is an hour. Both now say what actually happens, including which tags background dispatch deliberately does not cover and why. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 4 ++ README.md | 6 +- docs/play-release-notes.md | 119 ++++++++++++------------------------- 3 files changed, 45 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb91157..34965c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ ## [1.5.0] - 2026-08-30 +> **未上架 Google Play。** 這一版的內容隨 1.5.1 一起發布,商店的使用者是從 1.4.0 +> 直接更新到 1.5.1,所以 Play 的更新資訊涵蓋兩版(見 +> [docs/play-release-notes.md](docs/play-release-notes.md))。 + 這一版處理的是「我怎麼知道它有沒有在動」,以及兩個安靜到不像故障的故障。執行中的流程現在會在卡片、通知與長按閃電的清單裡同時現身;排程晚到十分鐘的原因是一個從來沒被要求過的權限;而 NexFlow 只要開著,就會把整支手機的 NFC 感應全部吃掉。 ### 新增(Added) diff --git a/README.md b/README.md index 86695a2..399337e 100644 --- a/README.md +++ b/README.md @@ -349,17 +349,17 @@ adb shell settings put global animator_duration_scale 1.0 | 亮度調整 | 修改系統設定(WRITE_SETTINGS) | | 勿擾模式 | 勿擾存取權限 | | Wi-Fi/飛航模式靜默切換 | `WRITE_SECURE_SETTINGS`(需透過 ADB 授權一次,App 內有指令可複製) | -| NFC 觸發 | 僅 App 在前景時有效 | +| NFC 觸發 | App 開著時皆可;關著時僅限 NDEF 標籤,且需有啟用中的 NFC 流程 | ## 已知限制(規劃中) - 全域變數需先在「設定 → 全域變數」建立才能寫入:`SET_VARIABLE` 不會用打錯的 `g:名稱` 自動建立新變數(避免 typo 悄悄產生殭屍變數)。設定框會擋下不存在的 `g:` 名稱、無法儲存;萬一仍寫入(例如匯入的舊檔),執行會**失敗並在執行記錄寫出該名稱**,而不是靜默跳過 - **MacroDroid 相容為部分覆蓋**:目前對照 22 種觸發、30 種動作、7 種條件的 MacroDroid class type(MacroDroid 本身有上百種),其餘一律轉成 `UNSUPPORTED` 並在匯入時逐項列出警告(原始 class 名會保留在 config 裡,方便手動補上對應動作)。設定欄位有 46 種 class 的對照,沒有對照或對不過去的欄位同樣會逐項寫進警告,不會靜默消失。轉換是 best-effort,複雜巨集匯入後請先檢視再啟用 —— 已知一定轉不過來的項目(地理圍欄座標、選單各選項的動作、捷徑、桌布圖片…)列在 [docs/MACRODROID_IMPORT.md](docs/MACRODROID_IMPORT.md) -- **精確時間需要「鬧鐘與提醒」權限**:TIME 觸發走 `AlarmManager`,Android 12+ 未授權 `SCHEDULE_EXACT_ALARM` 時會退回不精確排程 —— 仍能穿透 Doze,但系統可能併入維護視窗,**誤差數分鐘**。設定頁有引導前往授權 +- **精確時間需要「鬧鐘與提醒」權限**:TIME 觸發走 `AlarmManager`,Android 12+ 未授權 `SCHEDULE_EXACT_ALARM` 時會退回不精確排程 —— 仍能穿透 Doze,但系統可能併入維護視窗,**誤差可達一小時**(官方對不精確鬧鐘的保證是「一小時內」)。未授權時**流程卡片會亮出警告**並帶你前往授權,授權後既有排程會立即重新校正 - **省電機制可能中斷非時間類觸發**:搖晃、環境光、Wi-Fi/藍牙、耳機、螢幕等觸發依附於前景服務的事件串流,被系統或廠商的省電策略殺掉後就會停止監聽(TIME 因為由 AlarmManager 驅動不受影響)。目前**還沒有引導使用者把 App 加入電池最佳化白名單**的畫面,激進省電的機型請自行到系統設定放行 - **同一流程不會並行執行**:正在執行的流程再次被觸發會直接略過(避免重複觸發疊加成多份同時執行),這些觸發**不會排隊補跑** - **Wi-Fi/飛航模式靜默切換需一次性 ADB 授權**:Android 10 起系統不再開放第三方 App 直接切換,必須手動授予 `WRITE_SECURE_SETTINGS`(App 內可複製指令);未授權時只能改為跳轉系統設定頁 -- **NFC 觸發僅在 App 前景時有效**:使用 `enableReaderMode`,背景不會接收標籤 +- **NFC 背景觸發只涵蓋 NDEF 標籤**:App 開著時走 `enableReaderMode`,什麼標籤都讀得到。關著時走 `TECH_DISCOVERED`,tech-list 只列 `Ndef`/`NdefFormatable` —— 交通卡、門禁卡(MifareClassic)與感應支付(IsoDep)不會列舉 `Ndef`,所以**背景不會觸發**,這是刻意的:那份攔截若放寬,NexFlow 就會變成這些卡片的萬用接收者。另外,**寫入網址的標籤仍會開啟瀏覽器**(`NDEF_DISCOVERED` 優先權在上)。背景攔截掛在預設關閉的 `` 上,只有在你有啟用中的 NFC 流程時才會打開——沒有的話 NexFlow 完全不出現在標籤分派名單裡 ## 參與貢獻 diff --git a/docs/play-release-notes.md b/docs/play-release-notes.md index dbb9526..c140c36 100644 --- a/docs/play-release-notes.md +++ b/docs/play-release-notes.md @@ -6,133 +6,90 @@ - 語言代碼要與商店資訊已啟用的語言一致:`zh-TW` / `en-US` / `ja-JP` / `zh-CN`。 - 這是寫給使用者看的,不是 CHANGELOG。挑「使用者看得到、或需要使用者動手」的事就好; 完整的技術變更留在 [CHANGELOG.md](../CHANGELOG.md)。 -- 這一版最該講的是**排程權限**:那是唯一需要使用者自己去授權才會好的項目, - 其餘都是安裝完就生效。 --- -# 1.5.0 +# 上架用:1.5.1(versionCode 10) -## 繁體中文(zh-TW)— 223 字元 +**1.5.0 沒有上架 Play**,所以商店的使用者是從 1.4.0 直接跳到 1.5.1。下面這份文案 +因此涵蓋 1.5.0 與 1.5.1 兩版的內容,不是只有 1.5.1 的部分。 + +排在最前面的是**排程權限**以外唯一的新功能,而排程權限是這兩版裡唯一 +**需要使用者自己動手授權**才會好的項目——其餘都是安裝完就生效。 + +英文版塞不下音量摘要那一條(500 字元上限,英文最不省字),所以只有中日文保留; +那是四件事裡最小的一件,捨它最不虧。 + +## 繁體中文(zh-TW)— 232 字元 ``` 【看得見正在執行的流程】 -流程執行中,卡片右下角會顯示「執行中」。不論是側滑執行、時間到,或是背景觸發,都看得到。通知會寫出正在執行哪一個流程;長按右上角的閃電,可列出目前所有執行中的流程。 +流程執行時,卡片右下角會顯示「執行中」,不論是側滑執行、時間到或背景觸發。通知會寫出正在執行哪一個流程;長按右上角的閃電可列出全部。 【排程準時了】 -排定 9:00 卻晚十分鐘才跑,是因為缺少「鬧鐘和提醒」權限。流程卡片現在會提示並帶你前往授權,授權後既有排程立即重新校正。 +排定 9:00 卻晚十分鐘才跑,是因為缺少「鬧鐘和提醒」權限。流程卡片現在會提示,並帶你前往授權。 -【修正】 -• App 開著時不再獨占 NFC,感應網址標籤或交通卡恢復正常 +【NFC 修好了】 +App 開著時不再獨占感應,網址標籤與交通卡恢復正常;而在你有啟用中的 NFC 流程時,關著 App 感應一樣會觸發。 + +【其他】 • 音量動作的摘要不再顯示「?」 ``` --- -## English(en-US)— 497 字元 +## English(en-US)— 483 字元 ``` SEE WHAT'S RUNNING -A running flow now shows "Running" on its card — whether you swiped it, a schedule fired it, or a background trigger did. The notification names the flow, and a long press on the lightning bolt lists them all. +A running flow shows "Running" on its card — swiped, scheduled or triggered in the background. The notification names it, and a long press on the lightning bolt lists them all. SCHEDULES RUN ON TIME A flow set for 9:00 could run ten minutes late: the "Alarms & reminders" permission was never requested. Flows now flag it and take you there. -FIXED -• NFC is no longer taken over while the app is open -• Volume actions no longer summarise as "?" +NFC FIXED +Tags are no longer taken over while the app is open, and work with the app closed once an NFC flow is enabled. ``` --- -## 日本語(ja-JP)— 275 字元 +## 日本語(ja-JP)— 289 字元 ``` 【実行中のフローが見えます】 -フローの実行中、カード右下に「実行中」と表示されます。スワイプ実行でも、時刻やバックグラウンドのトリガーでも同じです。通知には実行中のフロー名が表示され、右上の稲妻を長押しすると実行中のフローが一覧できます。 +実行中はカード右下に「実行中」と表示されます。スワイプ実行でも、時刻やバックグラウンドのトリガーでも同じです。通知にはフロー名が表示され、右上の稲妻の長押しで一覧できます。 【スケジュールが時間どおりに】 -9:00 に設定したフローが10分遅れることがありました。「アラームとリマインダー」の権限を要求していなかったためです。不足時はカードに表示し、設定画面へ案内します。 +9:00 のフローが10分遅れることがありました。「アラームとリマインダー」の権限を要求していなかったためです。不足時はカードに表示し、設定へ案内します。 + +【NFC の修正】 +アプリ起動中にタグを占有しなくなりました。有効な NFC フローがあれば、アプリを閉じていてもタグが動作します。 -【修正】 -・アプリ起動中に NFC を占有しなくなりました ・音量アクションの概要が「?」にならないように ``` --- -## 简体中文(zh-CN)— 223 字元 +## 简体中文(zh-CN)— 232 字元 ``` 【看得见正在运行的流程】 -流程运行中,卡片右下角会显示「运行中」。无论是侧滑运行、时间到,还是后台触发,都看得到。通知会写出正在运行哪一个流程;长按右上角的闪电,可列出当前所有运行中的流程。 +流程运行时,卡片右下角会显示「运行中」,无论是侧滑运行、时间到或后台触发。通知会写出正在运行哪一个流程;长按右上角的闪电可列出全部。 【定时准时了】 -设定 9:00 却晚十分钟才跑,是因为缺少「闹钟和提醒」权限。流程卡片现在会提示并带你前往授权,授权后既有定时立即重新校正。 - -【修正】 -• App 开着时不再独占 NFC,感应网址标签或交通卡恢复正常 -• 音量动作的摘要不再显示「?」 -``` - ---- - -# 1.5.1 - -## 繁體中文(zh-TW)— 231 字元 - -``` -【關著 App 也能用 NFC 標籤觸發流程】 -1.5.0 為了不讓 NexFlow 攔截門禁卡、交通卡,拿掉了背景 NFC。現在補回來,但只發給真的需要的人:只有在你有「啟用中的 NFC 流程」時,NexFlow 才會登記接收標籤;沒有的話完全不會出現在感應名單裡。 +设定 9:00 却晚十分钟才跑,是因为缺少「闹钟和提醒」权限。流程卡片现在会提示,并带你前往授权。 -感應後不會跳出任何畫面,流程直接在背景執行。 - -【說明】 -• 寫了網址的標籤仍會開啟瀏覽器,這是系統的優先順序 -• 交通卡、門禁卡這類非 NDEF 標籤,背景不會觸發;App 開著時仍讀得到 -``` - ---- +【NFC 修好了】 +App 开着时不再独占感应,网址标签与交通卡恢复正常;而在你有启用中的 NFC 流程时,关着 App 感应一样会触发。 -## English(en-US)— 470 字元 - -``` -NFC TAGS WORK WITH THE APP CLOSED -1.5.0 dropped background NFC so NexFlow would stop intercepting door badges and transit cards. It's back, but only for people who need it: NexFlow registers for tags only while you have an enabled NFC flow, and otherwise never appears in tag handling at all. - -Scanning shows nothing on screen — the flow just runs. - -NOTES -• Tags carrying a URL still open the browser -• Non-NDEF tags such as transit cards won't trigger in the background -``` - ---- - -## 日本語(ja-JP)— 270 字元 - -``` -【アプリを閉じていても NFC タグでフローが動きます】 -1.5.0 では、社員証や交通系 IC カードを横取りしないよう、バックグラウンドの NFC を外していました。今回、必要な人にだけ戻します。有効な NFC フローがあるときだけタグの受け取りに登録し、なければ一覧に一切現れません。 - -タグをかざしても画面には何も出ず、フローだけが実行されます。 - -【注意】 -・URL が書かれたタグはブラウザが開きます(システムの優先順位) -・交通系 IC など NDEF でないタグはバックグラウンドでは動きません。アプリを開いていれば読めます +【其他】 +• 音量动作的摘要不再显示「?」 ``` --- -## 简体中文(zh-CN)— 231 字元 +# 附註 -``` -【关着 App 也能用 NFC 标签触发流程】 -1.5.0 为了不让 NexFlow 拦截门禁卡、交通卡,去掉了后台 NFC。现在补回来,但只发给真的需要的人:只有在你有「启用中的 NFC 流程」时,NexFlow 才会登记接收标签;没有的话完全不会出现在感应名单里。 - -感应后不会弹出任何画面,流程直接在后台执行。 - -【说明】 -• 写了网址的标签仍会打开浏览器,这是系统的优先顺序 -• 交通卡、门禁卡这类非 NDEF 标签,后台不会触发;App 开着时仍读得到 -``` +若日後需要分版本的文案(例如補發 GitHub Release),1.5.0 與 1.5.1 各自的四語版本 +保留在 git 記錄裡:`git show 618d26b:docs/play-release-notes.md`(1.5.0)與 +`git show 897abb1:docs/play-release-notes.md`(1.5.0 + 1.5.1 分開版)。