Repository: gkd-kit/gkd
Branch: main
Commit: 8fa475fc1dea
Files: 362
Total size: 1.5 MB
Directory structure:
gitextract_z6y5c1r5/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ └── workflows/
│ ├── Build-Apk.yml
│ └── Build-Release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ ├── schemas/
│ │ └── li.songe.gkd.db.AppDb/
│ │ ├── 1.json
│ │ ├── 10.json
│ │ ├── 11.json
│ │ ├── 12.json
│ │ ├── 13.json
│ │ ├── 14.json
│ │ ├── 2.json
│ │ ├── 3.json
│ │ ├── 4.json
│ │ ├── 5.json
│ │ ├── 6.json
│ │ ├── 7.json
│ │ ├── 8.json
│ │ └── 9.json
│ └── src/
│ ├── androidTest/
│ │ └── kotlin/
│ │ └── li/
│ │ └── songe/
│ │ └── gkd/
│ │ └── ExampleInstrumentedTest.kt
│ ├── gkd/
│ │ └── AndroidManifest.xml
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── aidl/
│ │ │ └── li/
│ │ │ └── songe/
│ │ │ └── gkd/
│ │ │ └── shizuku/
│ │ │ ├── CommandResult.aidl
│ │ │ └── IUserService.aidl
│ │ ├── kotlin/
│ │ │ ├── com/
│ │ │ │ └── google/
│ │ │ │ └── android/
│ │ │ │ └── accessibility/
│ │ │ │ └── selecttospeak/
│ │ │ │ └── SelectToSpeakService.kt
│ │ │ └── li/
│ │ │ └── songe/
│ │ │ └── gkd/
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── OpenFileActivity.kt
│ │ │ ├── OpenSchemeActivity.kt
│ │ │ ├── OpenTileActivity.kt
│ │ │ ├── a11y/
│ │ │ │ ├── A11yCommonImpl.kt
│ │ │ │ ├── A11yContext.kt
│ │ │ │ ├── A11yExt.kt
│ │ │ │ ├── A11yFeat.kt
│ │ │ │ ├── A11yRuleEngine.kt
│ │ │ │ └── A11yState.kt
│ │ │ ├── data/
│ │ │ │ ├── A11yEventLog.kt
│ │ │ │ ├── ActionLog.kt
│ │ │ │ ├── ActivityLog.kt
│ │ │ │ ├── AppConfig.kt
│ │ │ │ ├── AppInfo.kt
│ │ │ │ ├── AppRule.kt
│ │ │ │ ├── AppVisitLog.kt
│ │ │ │ ├── AttrInfo.kt
│ │ │ │ ├── BaseSnapshot.kt
│ │ │ │ ├── CategoryConfig.kt
│ │ │ │ ├── ComplexSnapshot.kt
│ │ │ │ ├── DeviceInfo.kt
│ │ │ │ ├── GithubPoliciesAsset.kt
│ │ │ │ ├── GkdAction.kt
│ │ │ │ ├── GlobalRule.kt
│ │ │ │ ├── NodeInfo.kt
│ │ │ │ ├── RawSubscription.kt
│ │ │ │ ├── ResolvedGroup.kt
│ │ │ │ ├── ResolvedRule.kt
│ │ │ │ ├── RpcError.kt
│ │ │ │ ├── Snapshot.kt
│ │ │ │ ├── SubsConfig.kt
│ │ │ │ ├── SubsItem.kt
│ │ │ │ ├── SubsVersion.kt
│ │ │ │ ├── TransferData.kt
│ │ │ │ ├── UserInfo.kt
│ │ │ │ └── Value.kt
│ │ │ ├── db/
│ │ │ │ └── AppDb.kt
│ │ │ ├── notif/
│ │ │ │ ├── Notif.kt
│ │ │ │ ├── NotifChannel.kt
│ │ │ │ └── StopServiceReceiver.kt
│ │ │ ├── permission/
│ │ │ │ ├── PermissionDialog.kt
│ │ │ │ └── PermissionState.kt
│ │ │ ├── service/
│ │ │ │ ├── A11yService.kt
│ │ │ │ ├── ActivityService.kt
│ │ │ │ ├── ActivityTileService.kt
│ │ │ │ ├── BaseTileService.kt
│ │ │ │ ├── ButtonService.kt
│ │ │ │ ├── ButtonTileService.kt
│ │ │ │ ├── EventService.kt
│ │ │ │ ├── EventTileService.kt
│ │ │ │ ├── ExposeService.kt
│ │ │ │ ├── GkdTileService.kt
│ │ │ │ ├── HttpService.kt
│ │ │ │ ├── HttpTileService.kt
│ │ │ │ ├── MatchTileService.kt
│ │ │ │ ├── OverlayWindowService.kt
│ │ │ │ ├── ScreenshotService.kt
│ │ │ │ ├── SnapshotTileService.kt
│ │ │ │ └── StatusService.kt
│ │ │ ├── shizuku/
│ │ │ │ ├── AccessibilityManager.kt
│ │ │ │ ├── ActivityManager.kt
│ │ │ │ ├── ActivityTaskManager.kt
│ │ │ │ ├── AppOpsService.kt
│ │ │ │ ├── AutomationService.kt
│ │ │ │ ├── CommandResult.kt
│ │ │ │ ├── HiddenCast.kt
│ │ │ │ ├── InputManager.kt
│ │ │ │ ├── InputShellCommand.kt
│ │ │ │ ├── PackageManager.kt
│ │ │ │ ├── ProxyUiAutomationConnection.kt
│ │ │ │ ├── ShizukuApi.kt
│ │ │ │ ├── TaskStackListener.kt
│ │ │ │ ├── UserManager.kt
│ │ │ │ ├── UserService.kt
│ │ │ │ └── WindowManager.kt
│ │ │ ├── store/
│ │ │ │ ├── SettingsStore.kt
│ │ │ │ ├── StorageExt.kt
│ │ │ │ └── StoreExt.kt
│ │ │ ├── ui/
│ │ │ │ ├── A11yEventLogPage.kt
│ │ │ │ ├── A11yEventLogVm.kt
│ │ │ │ ├── A11yScopeAppListPage.kt
│ │ │ │ ├── A11yScopeAppListVm.kt
│ │ │ │ ├── AboutPage.kt
│ │ │ │ ├── AboutVm.kt
│ │ │ │ ├── ActionLogPage.kt
│ │ │ │ ├── ActionLogVm.kt
│ │ │ │ ├── ActivityLogPage.kt
│ │ │ │ ├── ActivityLogVm.kt
│ │ │ │ ├── AdvancedPage.kt
│ │ │ │ ├── AdvancedVm.kt
│ │ │ │ ├── AppConfigPage.kt
│ │ │ │ ├── AppConfigVm.kt
│ │ │ │ ├── AppOpsAllowPage.kt
│ │ │ │ ├── AppOpsAllowVm.kt
│ │ │ │ ├── AuthA11yPage.kt
│ │ │ │ ├── AuthA11yVm.kt
│ │ │ │ ├── BlockA11yAppListPage.kt
│ │ │ │ ├── BlockA11yAppListVm.kt
│ │ │ │ ├── EditBlockAppListPage.kt
│ │ │ │ ├── EditBlockAppListVm.kt
│ │ │ │ ├── ImagePreviewPage.kt
│ │ │ │ ├── SlowGroupPage.kt
│ │ │ │ ├── SnapshotPage.kt
│ │ │ │ ├── SnapshotVm.kt
│ │ │ │ ├── SubsAppGroupListPage.kt
│ │ │ │ ├── SubsAppGroupListVm.kt
│ │ │ │ ├── SubsAppListPage.kt
│ │ │ │ ├── SubsAppListVm.kt
│ │ │ │ ├── SubsCategoryPage.kt
│ │ │ │ ├── SubsCategoryVm.kt
│ │ │ │ ├── SubsGlobalGroupExcludePage.kt
│ │ │ │ ├── SubsGlobalGroupExcludeVm.kt
│ │ │ │ ├── SubsGlobalGroupListPage.kt
│ │ │ │ ├── SubsGlobalGroupListVm.kt
│ │ │ │ ├── UpsertRuleGroupPage.kt
│ │ │ │ ├── UpsertRuleGroupVm.kt
│ │ │ │ ├── WebViewPage.kt
│ │ │ │ ├── component/
│ │ │ │ │ ├── AnimatedBooleanContent.kt
│ │ │ │ │ ├── AnimatedIcon.kt
│ │ │ │ │ ├── Animation.kt
│ │ │ │ │ ├── AnimationFloatingActionButton.kt
│ │ │ │ │ ├── AppBarTextField.kt
│ │ │ │ │ ├── AppCheckBoxCard.kt
│ │ │ │ │ ├── AppIcon.kt
│ │ │ │ │ ├── AppNameText.kt
│ │ │ │ │ ├── AuthButtonGroup.kt
│ │ │ │ │ ├── AuthCard.kt
│ │ │ │ │ ├── CopyTextCard.kt
│ │ │ │ │ ├── CustomIconButton.kt
│ │ │ │ │ ├── CustomOutlinedTextField.kt
│ │ │ │ │ ├── DialogOptions.kt
│ │ │ │ │ ├── EmptyText.kt
│ │ │ │ │ ├── FixedTimeText.kt
│ │ │ │ │ ├── FullscreenDialog.kt
│ │ │ │ │ ├── GroupNameText.kt
│ │ │ │ │ ├── Hooks.kt
│ │ │ │ │ ├── InnerDisableSwitch.kt
│ │ │ │ │ ├── InputSubsLinkOption.kt
│ │ │ │ │ ├── ManualAuthDialog.kt
│ │ │ │ │ ├── MenuExt.kt
│ │ │ │ │ ├── ModifierExt.kt
│ │ │ │ │ ├── MultiTextField.kt
│ │ │ │ │ ├── PerfCheckbox.kt
│ │ │ │ │ ├── PerfIcon.kt
│ │ │ │ │ ├── PerfSwitch.kt
│ │ │ │ │ ├── PerfTopAppBar.kt
│ │ │ │ │ ├── QueryPkgTipCard.kt
│ │ │ │ │ ├── RotatingLoadingIcon.kt
│ │ │ │ │ ├── RuleGroupCard.kt
│ │ │ │ │ ├── RuleGroupDialog.kt
│ │ │ │ │ ├── RuleGroupState.kt
│ │ │ │ │ ├── SettingItem.kt
│ │ │ │ │ ├── ShareDataDialog.kt
│ │ │ │ │ ├── SubsAppCard.kt
│ │ │ │ │ ├── SubsItemCard.kt
│ │ │ │ │ ├── SubsSheet.kt
│ │ │ │ │ ├── TermsAcceptDialog.kt
│ │ │ │ │ ├── TextDialog.kt
│ │ │ │ │ ├── TextMenu.kt
│ │ │ │ │ ├── TextSwitch.kt
│ │ │ │ │ ├── TooltipIconButtonBox.kt
│ │ │ │ │ ├── TowLineText.kt
│ │ │ │ │ └── UploadOptions.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── AppListPage.kt
│ │ │ │ │ ├── ControlPage.kt
│ │ │ │ │ ├── HomePage.kt
│ │ │ │ │ ├── HomeVm.kt
│ │ │ │ │ ├── ScaffoldExt.kt
│ │ │ │ │ ├── SettingsPage.kt
│ │ │ │ │ └── SubsManagePage.kt
│ │ │ │ ├── icon/
│ │ │ │ │ ├── BackCloseIcon.kt
│ │ │ │ │ ├── DragPan.kt
│ │ │ │ │ ├── LockOpenRight.kt
│ │ │ │ │ ├── ResetSettings.kt
│ │ │ │ │ └── SportsBasketball.kt
│ │ │ │ ├── share/
│ │ │ │ │ ├── AppFilter.kt
│ │ │ │ │ ├── BaseViewModel.kt
│ │ │ │ │ ├── FixedWindowInsets.kt
│ │ │ │ │ ├── ListPlaceholder.kt
│ │ │ │ │ ├── LocalExt.kt
│ │ │ │ │ ├── ModifierExt.kt
│ │ │ │ │ └── StateExt.kt
│ │ │ │ └── style/
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Padding.kt
│ │ │ │ ├── TextTransformation.kt
│ │ │ │ └── Theme.kt
│ │ │ └── util/
│ │ │ ├── AndroidTarget.kt
│ │ │ ├── AppInfoState.kt
│ │ │ ├── BarUtils.kt
│ │ │ ├── CollectionExt.kt
│ │ │ ├── Constants.kt
│ │ │ ├── CoroutineExt.kt
│ │ │ ├── FlowExt.kt
│ │ │ ├── FolderExt.kt
│ │ │ ├── Github.kt
│ │ │ ├── ImageUtils.kt
│ │ │ ├── IntentExt.kt
│ │ │ ├── KeyboardUtils.kt
│ │ │ ├── LifecycleCallbacks.kt
│ │ │ ├── LinkLoad.kt
│ │ │ ├── LoadStatus.kt
│ │ │ ├── LogUtils.kt
│ │ │ ├── MutexState.kt
│ │ │ ├── NetworkExt.kt
│ │ │ ├── NetworkUtils.kt
│ │ │ ├── Option.kt
│ │ │ ├── Others.kt
│ │ │ ├── ScreenUtils.kt
│ │ │ ├── ScreenshotUtil.kt
│ │ │ ├── Singleton.kt
│ │ │ ├── SnapshotExt.kt
│ │ │ ├── SubsState.kt
│ │ │ ├── TimeExt.kt
│ │ │ ├── Toast.kt
│ │ │ ├── Unit.kt
│ │ │ ├── Upgrade.kt
│ │ │ ├── UriUtils.kt
│ │ │ └── ZipUtils.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_anim_logo.xml
│ │ │ ├── ic_anim_search_close.xml
│ │ │ ├── ic_capture.xml
│ │ │ ├── ic_event_list.xml
│ │ │ ├── ic_flash_off.xml
│ │ │ ├── ic_flash_on.xml
│ │ │ ├── ic_http.xml
│ │ │ ├── ic_launcher.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── ic_layers.xml
│ │ │ ├── ic_page_info.xml
│ │ │ ├── ic_radio_button.xml
│ │ │ └── ic_status.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── values-night/
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── ab_desc.xml
│ │ ├── file_paths.xml
│ │ └── network_security_config.xml
│ └── test/
│ └── kotlin/
│ └── li/
│ └── songe/
│ └── gkd/
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── hidden_api/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ ├── android/
│ │ ├── accessibilityservice/
│ │ │ ├── AccessibilityServiceInfoHidden.java
│ │ │ └── IAccessibilityServiceClient.java
│ │ ├── app/
│ │ │ ├── AppOpsManagerHidden.java
│ │ │ ├── IActivityManager.java
│ │ │ ├── IActivityTaskManager.java
│ │ │ ├── IApplicationThread.java
│ │ │ ├── ITaskStackListener.java
│ │ │ ├── IUiAutomationConnection.java
│ │ │ └── UiAutomationHidden.java
│ │ ├── content/
│ │ │ ├── ContextHidden.java
│ │ │ └── pm/
│ │ │ ├── IPackageManager.java
│ │ │ ├── PackageInfoHidden.java
│ │ │ ├── ParceledListSlice.java
│ │ │ └── UserInfo.java
│ │ ├── hardware/
│ │ │ └── input/
│ │ │ ├── IInputManager.java
│ │ │ └── InputManagerHidden.java
│ │ ├── os/
│ │ │ └── IUserManager.java
│ │ ├── view/
│ │ │ ├── IWindowManager.java
│ │ │ ├── KeyEventHidden.java
│ │ │ ├── MotionEventHidden.java
│ │ │ ├── SurfaceControlHidden.java
│ │ │ └── accessibility/
│ │ │ ├── AccessibilityNodeInfoHidden.java
│ │ │ └── IAccessibilityManager.java
│ │ └── window/
│ │ └── ScreenCapture.java
│ └── com/
│ └── android/
│ └── internal/
│ ├── R.java
│ └── app/
│ └── IAppOpsService.java
├── selector/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/
│ │ └── kotlin/
│ │ └── li/
│ │ └── songe/
│ │ └── selector/
│ │ ├── Exception.kt
│ │ ├── FastQuery.kt
│ │ ├── MatchOption.kt
│ │ ├── QueryContext.kt
│ │ ├── QueryPath.kt
│ │ ├── QueryResult.kt
│ │ ├── Selector.kt
│ │ ├── Stringify.kt
│ │ ├── Transform.kt
│ │ ├── TypeInfo.kt
│ │ ├── connect/
│ │ │ ├── ConnectExpression.kt
│ │ │ ├── ConnectOperator.kt
│ │ │ ├── ConnectSegment.kt
│ │ │ ├── ConnectWrapper.kt
│ │ │ ├── PolynomialExpression.kt
│ │ │ └── TupleExpression.kt
│ │ ├── parser/
│ │ │ ├── AstNode.kt
│ │ │ ├── AstParser.kt
│ │ │ ├── BaseParser.kt
│ │ │ ├── ConnectParser.kt
│ │ │ ├── PropertyParser.kt
│ │ │ └── SelectorParser.kt
│ │ ├── property/
│ │ │ ├── BinaryExpression.kt
│ │ │ ├── CompareOperator.kt
│ │ │ ├── Expression.kt
│ │ │ ├── LogicalExpression.kt
│ │ │ ├── LogicalOperator.kt
│ │ │ ├── NotExpression.kt
│ │ │ ├── PropertySegment.kt
│ │ │ ├── PropertyUnit.kt
│ │ │ ├── PropertyWrapper.kt
│ │ │ └── ValueExpression.kt
│ │ ├── toMatches.kt
│ │ ├── unit/
│ │ │ ├── LogicalSelectorExpression.kt
│ │ │ ├── NotSelectorExpression.kt
│ │ │ ├── SelectorExpression.kt
│ │ │ ├── SelectorLogicalOperator.kt
│ │ │ └── UnitSelectorExpression.kt
│ │ └── util.kt
│ ├── jsMain/
│ │ └── kotlin/
│ │ └── li/
│ │ └── songe/
│ │ └── selector/
│ │ └── toMatches.js.kt
│ ├── jvmMain/
│ │ └── kotlin/
│ │ └── li/
│ │ └── songe/
│ │ └── selector/
│ │ └── toMatches.jvm.kt
│ └── jvmTest/
│ └── kotlin/
│ └── li/
│ └── songe/
│ └── selector/
│ ├── ParserUnitTest.kt
│ ├── QueryUnitTest.kt
│ ├── Snapshot.kt
│ └── TypeUnitTest.kt
├── settings.gradle.kts
└── stability_config.conf
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
# github: [lisonge]
custom: ['https://github.com/lisonge/sponsor']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 错误反馈 / Bug report
title: "[BUG] "
description: 反馈你遇到的错误 / Report the bug you encountered
labels: ["pending triage"]
body:
- type: markdown
attributes:
value: |
## 感谢您花时间填写,在提交问题之前,请确保您完成以下操作
1. 请 **确保** 您已经查阅了 [GKD 官方文档](https://gkd.li) 以及 [常见问题](https://gkd.li/guide/faq)
2. 请 **判断** 是不是第三方规则订阅的问题,如果是,你应该向规则提供者反馈,而不是在这里提交,**此处只接受 GKD 应用本体的问题**
3. 请 **确保** 已有的 [问题](https://github.com/gkd-kit/gkd/issues?q=is%3Aissue) 或 [讨论](https://github.com/orgs/gkd-kit/discussions?discussions_q=) 中没有人提交过相似问题,否则请在该问题下进行讨论
4. 请 **不要** 开启重复相关的 issue,这将导致别人搜索 issue 时出现无关的低质量信息,否则你的问题将会被直接关闭甚至删除
5. 请 **确保** 你的问题能在 [releases](https://github.com/gkd-kit/gkd/releases/latest) 发布的最新版本(包含测试版本)上复现 (如果不是请先更新到最新版本复现后再提交问题)
6. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
- type: textarea
id: log-file
attributes:
label: |
日志文件
description: |
首页-设置-关于-日志,上传日志文件或生成链接并粘贴到下面的输入框\
任何问题都需要提供日志文件. 否则将直接关闭,请不要纯发文字/截图/视频
validations:
required: true
- type: textarea
id: bug-1
attributes:
label: |
BUG描述(文字/截图/视频)
description: |
请使用尽量准确的描述,否则你的问题将会被直接关闭\
另外如果你的问题是关于快照/选择器的,请必须提供快照链接或者快照文件,否则你的问题将会被直接关闭
validations:
required: true
- type: textarea
id: bug-2
attributes:
label: |
期望行为(文字/截图/视频)
description: |
请使用尽量准确的描述,否则你的问题将会被直接关闭
validations:
required: true
- type: textarea
id: bug-3
attributes:
label: |
实际行为(文字/截图/视频)
description: |
请使用尽量准确的描述,否则你的问题将会被直接关闭
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 讨论交流 / Discussions
url: https://github.com/gkd-kit/gkd/discussions
about: '其他提问或请求: 选择器/规则/快照/无障碍/设备/相关问题. 在 issues 搜索不到可前往搜索讨论'
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 功能请求 / Feature request
title: "[Feature] "
description: 提出你的功能请求 / Propose your feature request
labels: ["pending triage"]
body:
- type: markdown
attributes:
value: |
## 感谢您花时间填写,在提交问题之前,请确保您完成以下操作
1. GKD 默认不提供任何规则,你可以查看 [GKD 官方文档](https://gkd.li) 后自行编写规则或者导入远程订阅,请不要再提出类似想要XXX规则这种问题
2. 请 **判断** 是不是第三方规则订阅的功能请求,如果是,你应该向规则提供者反馈,而不是在这里提交,**此处只接受 GKD 应用本体的功能请求**
3. 请 **确保** 已有的 [问题](https://github.com/gkd-kit/gkd/issues?q=is%3Aissue) 或 [讨论](https://github.com/orgs/gkd-kit/discussions?discussions_q=) 中没有人提交过相似问题,否则请在该问题下进行讨论
4. 请 **不要** 开启重复相关的 issue,这将导致别人搜索 issue 时出现无关的低质量信息,否则你的问题将会被直接关闭甚至删除
5. 请 **确保** 你想要的功能在 [releases](https://github.com/gkd-kit/gkd/releases/latest) 发布的最新版本(包含测试版本)上没有找到 (如果不是请先更新到最新版本验证后再提交问题)
6. 请 **务必** 给 issue 填写一个简洁明了的标题,以便他人快速检索
- type: textarea
id: feature-description
attributes:
label: |
新功能描述
description: |
例如: 我希望在 GKD 中的什么页面添加什么功能,以及这个功能的作用是什么\
或者在规则定义中添加某个字段,以及这个字段的作用是什么\
请使用准确的描述,否则你的问题将会被直接关闭
validations:
required: true
================================================
FILE: .github/workflows/Build-Apk.yml
================================================
name: Build-Apk
on:
workflow_dispatch:
push:
branches:
- '**'
paths-ignore:
- 'LICENSE'
- '*.md'
- '.github/**'
jobs:
build:
if: ${{ !startsWith(github.event.head_commit.message, 'chore:') && !startsWith(github.event.head_commit.message, 'chore(') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '21'
- uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
- name: write gkd secrets info
run: |
echo ${{ secrets.GKD_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/gkd.jks
echo GKD_STORE_FILE='${{ github.workspace }}/gkd.jks' >> gradle.properties
echo GKD_STORE_PASSWORD='${{ secrets.GKD_STORE_PASSWORD }}' >> gradle.properties
echo GKD_KEY_ALIAS='${{ secrets.GKD_KEY_ALIAS }}' >> gradle.properties
echo GKD_KEY_PASSWORD='${{ secrets.GKD_KEY_PASSWORD }}' >> gradle.properties
- run: chmod 777 ./gradlew
- run: ./gradlew app:assembleGkdRelease
- uses: actions/upload-artifact@v4
with:
name: release
path: app/build/outputs/apk/gkd/release
- uses: actions/upload-artifact@v4
with:
name: outputs
path: app/build/outputs
================================================
FILE: .github/workflows/Build-Release.yml
================================================
name: Build-Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '21'
- uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
- name: write gkd secrets info
run: |
echo ${{ secrets.GKD_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/gkd.jks
echo GKD_STORE_FILE='${{ github.workspace }}/gkd.jks' >> gradle.properties
echo GKD_STORE_PASSWORD='${{ secrets.GKD_STORE_PASSWORD }}' >> gradle.properties
echo GKD_KEY_ALIAS='${{ secrets.GKD_KEY_ALIAS }}' >> gradle.properties
echo GKD_KEY_PASSWORD='${{ secrets.GKD_KEY_PASSWORD }}' >> gradle.properties
- name: write play secrets info
run: |
echo ${{ secrets.PLAY_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/play.jks
echo PLAY_STORE_FILE='${{ github.workspace }}/play.jks' >> gradle.properties
echo PLAY_STORE_PASSWORD='${{ secrets.PLAY_STORE_PASSWORD }}' >> gradle.properties
echo PLAY_KEY_ALIAS='${{ secrets.PLAY_KEY_ALIAS }}' >> gradle.properties
echo PLAY_KEY_PASSWORD='${{ secrets.PLAY_KEY_PASSWORD }}' >> gradle.properties
- run: chmod 777 ./gradlew
- run: ./gradlew app:assembleGkdRelease app:bundlePlayRelease
- uses: actions/upload-artifact@v4
with:
name: release
path: app/build/outputs/apk/gkd/release
- uses: actions/upload-artifact@v4
with:
name: playRelease
path: app/build/outputs/bundle/playRelease
- uses: actions/upload-artifact@v4
with:
name: outputs
path: app/build/outputs
- uses: actions/upload-artifact@v4
with:
name: CHANGELOG.md
path: CHANGELOG.md
release:
needs: build
permissions: write-all
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: outputs
path: outputs
- uses: actions/download-artifact@v4
with:
name: release
path: release
- uses: actions/download-artifact@v4
with:
name: CHANGELOG.md
- run: ls -R
- id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
body_path: ./CHANGELOG.md
prerelease: ${{ contains(github.ref_name, 'beta') }}
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: release/app-gkd-release.apk
asset_name: gkd-${{ github.ref_name }}.apk
asset_content_type: application/vnd.android.package-archive
- run: zip -r outputs.zip outputs
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: outputs.zip
asset_name: outputs-${{ github.ref_name }}.zip
asset_content_type: application/zip
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
.idea
/kotlin-js-store
.vscode
*.jks
*.keystore
/_assets
/.kotlin
/gradle/libs.versions.updates.toml
================================================
FILE: CHANGELOG.md
================================================
# v1.11.6
以下是本次更新的主要内容
## 优化和修复
- 新增权限受限提示
- 新增应用列表分组筛选
- 新增图标按钮长按提示
- 优化触发记录日期显示
- 修复某些情况下界面背景颜色异常
## 更新方式
- GKD - 设置 - 关于 - 检测更新
- 下列方式之一


================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
# gkd
基于 [高级选择器](https://gkd.li/guide/selector) + [订阅规则](https://gkd.li/guide/subscription) + [快照审查](https://github.com/gkd-kit/inspect) 的自定义屏幕点击 Android 应用
通过自定义规则,在指定界面,满足指定条件(如屏幕上存在特定文字)时,点击特定的节点或位置或执行其他操作
- **快捷操作**
帮助你简化一些重复的流程, 如某些软件自动确认电脑登录
- **跳过流程**
某些软件可能在启动时存在一些烦人的流程, 这个软件可以帮助你点击跳过这个流程
## 免责声明
**本项目遵循 [GPL-3.0](/LICENSE) 开源,项目仅供学习交流,禁止用于商业或非法用途**
## 安装


如遇问题请先查看 [疑难解答](https://gkd.li/guide/faq)
## 截图
| | | | |
| ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- |
|  |  |  |  |
|  |  |  |  |
## 订阅
GKD **默认不提供规则**,需自行添加本地规则,或者通过订阅链接的方式获取远程规则
也可通过 [subscription-template](https://github.com/gkd-kit/subscription-template) 快速构建自己的远程订阅
第三方订阅列表可在 查看
要加入此列表, 需点击仓库主页右上角设置图标后在 Topics 中添加 `gkd-subscription`
示例图片 - 添加至 Topics (点击展开)

## 选择器
一个类似 CSS 选择器的选择器, 能联系节点上下文信息, 更容易也更精确找到目标节点
[@[vid=\"menu\"] < [vid=\"menu_container\"] - [vid=\"dot_text_layout\"] > [text^=\"广告\"]](https://i.gkd.li/i/14881985?gkd=QFt2aWQ9Im1lbnUiXSA8IFt2aWQ9Im1lbnVfY29udGFpbmVyIl0gLSBbdmlkPSJkb3RfdGV4dF9sYXlvdXQiXSA-IFt0ZXh0Xj0i5bm_5ZGKIl0)
示例图片 - 选择器路径视图 (点击展开)
[](https://i.gkd.li/i/14881985?gkd=QFt2aWQ9Im1lbnUiXSA8IFt2aWQ9Im1lbnVfY29udGFpbmVyIl0gLSBbdmlkPSJkb3RfdGV4dF9sYXlvdXQiXSA-IFt0ZXh0Xj0i5bm_5ZGKIl0)
## 捐赠
如果 GKD 对你有用, 可以通过以下链接支持该项目
或前往 [Google Play](https://play.google.com/store/apps/details?id=li.songe.gkd) 给个好评
## Star History
[](https://starchart.cc/gkd-kit/gkd)
================================================
FILE: app/.gitignore
================================================
/build
/release
================================================
FILE: app/build.gradle.kts
================================================
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import kotlin.reflect.full.declaredMemberProperties
fun String.runCommand(): String {
val process = ProcessBuilder(split(" "))
.redirectErrorStream(true)
.start()
val output = process.inputStream.bufferedReader().readText().trim()
val exitCode = process.waitFor()
if (exitCode != 0) {
error("Command failed with exit code $exitCode: $output")
}
return output
}
data class GitInfo(
val commitId: String,
val commitTime: String,
val tagName: String?,
)
val gitInfo = GitInfo(
commitId = "git rev-parse HEAD".runCommand(),
commitTime = "git log -1 --format=%ct".runCommand() + "000",
tagName = runCatching { "git describe --tags --exact-match".runCommand() }.getOrNull(),
)
val debugSuffixPairList by lazy {
javax.xml.parsers.DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(file("$projectDir/src/main/res/values/strings.xml"))
.documentElement.getElementsByTagName("string").run {
(0 until length).mapNotNull { i ->
val node = item(i)
if (node.attributes.getNamedItem("debug_suffix") != null) {
val key = node.attributes.getNamedItem("name").nodeValue
val value = node.textContent
key to value
} else {
null
}
}
}
}
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.androidx.room)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlinx.atomicfu)
alias(libs.plugins.google.ksp)
alias(libs.plugins.rikka.refine)
alias(libs.plugins.loc)
}
android {
namespace = rootProject.ext["android.namespace"].toString()
compileSdk = rootProject.ext["android.compileSdk"] as Int
buildToolsVersion = rootProject.ext["android.buildToolsVersion"].toString()
defaultConfig {
minSdk = rootProject.ext["android.minSdk"] as Int
targetSdk = rootProject.ext["android.targetSdk"] as Int
applicationId = "li.songe.gkd"
versionCode = 81
versionName = "1.11.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
androidResources {
localeFilters += listOf("zh", "en")
}
ndk {
// noinspection ChromeOsAbiSupport
abiFilters += listOf("arm64-v8a", "x86_64")
}
GitInfo::class.declaredMemberProperties.onEach {
manifestPlaceholders[it.name] = it.get(gitInfo) ?: ""
}
}
buildFeatures {
compose = true
aidl = true
resValues = true
}
val gkdSigningConfig = signingConfigs.create("gkd") {
storeFile = file(project.properties["GKD_STORE_FILE"] as String)
storePassword = project.properties["GKD_STORE_PASSWORD"].toString()
keyAlias = project.properties["GKD_KEY_ALIAS"].toString()
keyPassword = project.properties["GKD_KEY_PASSWORD"].toString()
}
val playSigningConfig = if (project.hasProperty("PLAY_STORE_FILE")) {
signingConfigs.create("play") {
storeFile = file(project.properties["PLAY_STORE_FILE"].toString())
storePassword = project.properties["PLAY_STORE_PASSWORD"].toString()
keyAlias = project.properties["PLAY_KEY_ALIAS"].toString()
keyPassword = project.properties["PLAY_KEY_PASSWORD"].toString()
}
} else {
gkdSigningConfig
}
buildTypes {
all {
if (gitInfo.tagName == null) {
versionNameSuffix = "-${gitInfo.commitId.take(7)}"
}
}
release {
isMinifyEnabled = true
isShrinkResources = true
isDebuggable = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
debug {
signingConfig = gkdSigningConfig
applicationIdSuffix = ".debug"
resValue("color", "better_black", "#FF5D92")
debugSuffixPairList.onEach { (key, value) ->
resValue("string", key, "$value-debug")
}
}
}
productFlavors {
flavorDimensions += "channel"
create("gkd") {
isDefault = true
signingConfig = gkdSigningConfig
resValue("bool", "is_accessibility_tool", "true")
}
create("play") {
signingConfig = playSigningConfig
resValue("bool", "is_accessibility_tool", "false")
}
all {
dimension = flavorDimensions.first()
manifestPlaceholders["channel"] = name
}
}
compileOptions {
sourceCompatibility = rootProject.ext["android.javaVersion"] as JavaVersion
targetCompatibility = rootProject.ext["android.javaVersion"] as JavaVersion
}
dependenciesInfo.includeInApk = false
packaging.resources.excludes += setOf(
// https://github.com/Kotlin/kotlinx.coroutines/issues/2023
"META-INF/**", "**/attach_hotspot_windows.dll",
"**.properties", "**.bin", "**/*.proto",
"**/kotlin-tooling-metadata.json",
// ktor
"**/custom.config.conf",
"**/custom.config.yaml",
)
}
kotlin {
compilerOptions {
jvmTarget.set(rootProject.ext["kotlin.jvmTarget"] as JvmTarget)
freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-Xcontext-parameters",
"-XXLanguage:+MultiDollarInterpolation",
)
}
}
// https://developer.android.com/jetpack/androidx/releases/room?hl=zh-cn#compiler-options
room {
schemaDirectory("$projectDir/schemas")
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
stabilityConfigurationFiles.addAll(
rootProject.layout.projectDirectory.file("stability_config.conf"),
)
}
loc {
template = "{packageName}.{methodName}({fileName}:{lineNumber})"
}
dependencies {
implementation(libs.kotlin.stdlib)
implementation(project(":selector"))
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.service)
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.animation)
implementation(libs.compose.animation.graphics)
implementation(libs.compose.icons)
implementation(libs.compose.preview)
debugImplementation(libs.compose.tooling)
androidTestImplementation(libs.compose.junit4)
implementation(libs.compose.activity)
implementation(libs.compose.material3)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.material3.adaptive.navigation3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso)
compileOnly(project(":hidden_api"))
implementation(libs.rikka.shizuku.api)
implementation(libs.rikka.shizuku.provider)
implementation(libs.lsposed.hiddenapibypass)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.paging)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.paging.compose)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.cio)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.google.accompanist.drawablepainter)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
// https://github.com/Kotlin/kotlinx-atomicfu/issues/145
implementation(libs.kotlinx.atomicfu)
implementation(libs.activityResultLauncher)
implementation(libs.reorderable)
implementation(libs.androidx.splashscreen)
implementation(libs.coil.compose)
implementation(libs.coil.network)
implementation(libs.coil.gif)
implementation(libs.exp4j)
implementation(libs.toaster)
implementation(libs.permissions)
implementation(libs.device)
implementation(libs.json5)
compileOnly(libs.loc.annotation)
implementation(libs.kevinnzouWebview)
}
================================================
FILE: app/proguard-rules.pro
================================================
# http://developer.android.com/guide/developing/tools/proguard.html
-dontwarn **
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/1.json
================================================
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e755c87d937b753ba02d02e8fa9afcec",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e755c87d937b753ba02d02e8fa9afcec')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/10.json
================================================
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "f3e8d3fb1a6de3876a6fceb921a456a2",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "action_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "app_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3e8d3fb1a6de3876a6fceb921a456a2')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/11.json
================================================
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "8cc58f95916481333213b24587c5d4d3",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT"
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "action_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8cc58f95916481333213b24587c5d4d3')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/12.json
================================================
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "58d6b0ebb55bc58ac6016a2b675e3ac4",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "action_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '58d6b0ebb55bc58ac6016a2b675e3ac4')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/13.json
================================================
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "f57629976fb6ff444f59487622f93814",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "action_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_visit_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `mtime` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f57629976fb6ff444f59487622f93814')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/14.json
================================================
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "8c34795e4b3ae52bf0188358d7bd3037",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "action_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_visit_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `mtime` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "a11y_event_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `appId` TEXT NOT NULL, `name` TEXT NOT NULL, `desc` TEXT, `text` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "appId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "desc",
"columnName": "desc",
"affinity": "TEXT"
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8c34795e4b3ae52bf0188358d7bd3037')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/2.json
================================================
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "3e6e28a11589fe6c2d8aff5b9467a489",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e6e28a11589fe6c2d8aff5b9467a489')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/3.json
================================================
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "b52c1f25e2052865818be5151b6ac6a0",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b52c1f25e2052865818be5151b6ac6a0')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/4.json
================================================
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "d1a618bf8475b588793fb1d201815a08",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd1a618bf8475b588793fb1d201815a08')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/5.json
================================================
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "4219672d163fce6e91926d9e15fc0e64",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4219672d163fce6e91926d9e15fc0e64')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/6.json
================================================
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "6cbd7772c779598a5448b9d5dc36c524",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6cbd7772c779598a5448b9d5dc36c524')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/7.json
================================================
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "9d5e657834ed630ac5cf00753cf24a55",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "activity_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9d5e657834ed630ac5cf00753cf24a55')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/8.json
================================================
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "409bb51310bcdb55ea721a8e88b6cef6",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "click_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '409bb51310bcdb55ea721a8e88b6cef6')"
]
}
}
================================================
FILE: app/schemas/li.songe.gkd.db.AppDb/9.json
================================================
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "343aa23eaf071a84fab19c5979d95f13",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mtime",
"columnName": "mtime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableUpdate",
"columnName": "enable_update",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "snapshot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appName",
"columnName": "app_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appVersionCode",
"columnName": "app_version_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "appVersionName",
"columnName": "app_version_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "screenHeight",
"columnName": "screen_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "screenWidth",
"columnName": "screen_width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLandscape",
"columnName": "is_landscape",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "githubAssetId",
"columnName": "github_asset_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "exclude",
"columnName": "exclude",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "category_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subsItemId",
"columnName": "subs_item_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryKey",
"columnName": "category_key",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "action_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subsId",
"columnName": "subs_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subsVersion",
"columnName": "subs_version",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "groupKey",
"columnName": "group_key",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "groupType",
"columnName": "group_type",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "2"
},
{
"fieldPath": "ruleIndex",
"columnName": "rule_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleKey",
"columnName": "rule_key",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "activity_log_v2",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ctime",
"columnName": "ctime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityId",
"columnName": "activity_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '343aa23eaf071a84fab19c5979d95f13')"
]
}
}
================================================
FILE: app/src/androidTest/kotlin/li/songe/gkd/ExampleInstrumentedTest.kt
================================================
package li.songe.gkd
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("li.songe.gkd", appContext.packageName)
}
}
================================================
FILE: app/src/gkd/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/aidl/li/songe/gkd/shizuku/CommandResult.aidl
================================================
package li.songe.gkd.shizuku;
parcelable CommandResult;
================================================
FILE: app/src/main/aidl/li/songe/gkd/shizuku/IUserService.aidl
================================================
package li.songe.gkd.shizuku;
import android.graphics.Bitmap;
import android.graphics.Rect;
import li.songe.gkd.shizuku.CommandResult;
interface IUserService {
void destroy() = 16777114; // Destroy method defined by Shizuku server
void exit() = 1;
CommandResult execCommand(String command) = 2;
Bitmap takeScreenshot1(int width, int height) = 3;
Bitmap takeScreenshot2(in Rect crop, int rotation) = 4;
Bitmap takeScreenshot3(in Rect crop) = 5;
int killLegacyService() = 6;
}
================================================
FILE: app/src/main/kotlin/com/google/android/accessibility/selecttospeak/SelectToSpeakService.kt
================================================
package com.google.android.accessibility.selecttospeak
import li.songe.gkd.service.A11yService
// https://github.com/ven-coder/Assists/issues/12#issuecomment-2684469065
class SelectToSpeakService : A11yService()
================================================
FILE: app/src/main/kotlin/li/songe/gkd/App.kt
================================================
package li.songe.gkd
import android.app.ActivityManager
import android.app.AppOpsManager
import android.app.Application
import android.app.KeyguardManager
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.database.ContentObserver
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.view.inputmethod.InputMethodManager
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import li.songe.gkd.a11y.initA11yFeat
import li.songe.gkd.data.selfAppInfo
import li.songe.gkd.notif.initChannel
import li.songe.gkd.service.clearHttpSubs
import li.songe.gkd.service.initA11yWhiteAppList
import li.songe.gkd.shizuku.initShizuku
import li.songe.gkd.store.initStore
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.PKG_FLAGS
import li.songe.gkd.util.initAppState
import li.songe.gkd.util.initSubsState
import li.songe.gkd.util.initToast
import li.songe.gkd.util.toast
import org.lsposed.hiddenapibypass.HiddenApiBypass
import kotlin.system.exitProcess
val appScope by lazy { MainScope() }
private lateinit var innerApp: App
val app: App
get() = innerApp
private val applicationInfo by lazy {
app.packageManager.getApplicationInfo(
app.packageName,
PackageManager.GET_META_DATA
)
}
private fun getMetaString(key: String): String {
return applicationInfo.metaData.getString(key) ?: error("Missing meta-data: $key")
}
// https://github.com/android-cs/16/blob/main/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java#L41
private const val ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':'
@Serializable
data class AppMeta(
val channel: String = getMetaString("channel"),
val commitId: String = getMetaString("commitId"),
val commitTime: Long = getMetaString("commitTime").toLong(),
val tagName: String? = getMetaString("tagName").takeIf { it.isNotEmpty() },
val debuggable: Boolean = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0,
val versionCode: Int = selfAppInfo.versionCode,
val versionName: String = selfAppInfo.versionName!!,
val appId: String = app.packageName!!,
val appName: String = app.getString(R.string.app_name)
) {
val commitUrl = "https://github.com/gkd-kit/gkd/".run {
plus(if (tagName != null) "tree/$tagName" else "commit/$commitId")
}
val isGkdChannel get() = channel == "gkd"
val updateEnabled get() = isGkdChannel
val isBeta get() = versionName.contains("beta")
}
val META by lazy { AppMeta() }
fun contentObserver(listener: () -> Unit) = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) = listener()
}
class App : Application() {
companion object {
const val START_WAIT_TIME = 3000L
}
init {
innerApp = this
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
if (AndroidTarget.P) {
HiddenApiBypass.addHiddenApiExemptions("L")
}
}
fun registerObserver(
uri: Uri,
observer: ContentObserver
) {
contentResolver.registerContentObserver(uri, false, observer)
}
fun unregisterObserver(observer: ContentObserver) {
contentResolver.unregisterContentObserver(observer)
}
fun getSecureString(name: String): String? = Settings.Secure.getString(contentResolver, name)
fun putSecureString(name: String, value: String?): Boolean {
return Settings.Secure.putString(contentResolver, name, value)
}
fun putSecureInt(name: String, value: Int): Boolean {
return Settings.Secure.putInt(contentResolver, name, value)
}
fun getSecureA11yServices(): MutableSet {
val value = getSecureString(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
if (value.isNullOrEmpty()) return mutableSetOf()
return value.split(
ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
).mapNotNull { ComponentName.unflattenFromString(it) }.toHashSet()
}
fun putSecureA11yServices(services: Set) {
putSecureString(
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
services.joinToString(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR.toString()) { it.flattenToShortString() }
)
}
fun resolveAppId(intent: Intent): String? {
return intent.resolveActivity(packageManager)?.packageName
}
fun getPkgInfo(appId: String): PackageInfo? = try {
packageManager.getPackageInfo(appId, PKG_FLAGS)
} catch (_: PackageManager.NameNotFoundException) {
null
}
fun resolveAppId(action: String, category: String? = null): String? {
val intent = Intent(action)
if (category != null) {
intent.addCategory(category)
}
return resolveAppId(intent)
}
fun startLaunchActivity() {
val intent = packageManager.getLaunchIntentForPackage(META.appId)!!
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_CLEAR_TOP
or Intent.FLAG_ACTIVITY_CLEAR_TASK
)
startActivity(intent)
}
fun checkGrantedPermission(permission: String) = ContextCompat.checkSelfPermission(
this,
permission,
) == PackageManager.PERMISSION_GRANTED
val startTime = System.currentTimeMillis()
var justStarted: Boolean = true
get() {
if (field) {
field = System.currentTimeMillis() - startTime < START_WAIT_TIME
}
return field
}
val activityManager by lazy { app.getSystemService(ACTIVITY_SERVICE) as ActivityManager }
val appOpsManager by lazy { app.getSystemService(APP_OPS_SERVICE) as AppOpsManager }
val inputMethodManager by lazy { app.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager }
val windowManager by lazy { app.getSystemService(WINDOW_SERVICE) as WindowManager }
val keyguardManager by lazy { app.getSystemService(KEYGUARD_SERVICE) as KeyguardManager }
val clipboardManager by lazy { app.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager }
val powerManager by lazy { getSystemService(POWER_SERVICE) as PowerManager }
val a11yManager by lazy { getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager }
override fun onCreate() {
super.onCreate()
LogUtils.d()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
toast(e.message ?: e.toString())
LogUtils.d("UncaughtExceptionHandler", t, e)
appScope.launch(Dispatchers.IO) {
delay(1500)
if (isActivityVisible) {
startLaunchActivity()
}
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
}
}
initToast()
initStore()
initChannel()
initAppState()
initA11yFeat()
initShizuku()
initSubsState()
initA11yWhiteAppList()
clearHttpSubs()
syncFixState()
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/MainActivity.kt
================================================
package li.songe.gkd
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.dylanc.activityresult.launcher.PickContentLauncher
import com.dylanc.activityresult.launcher.StartActivityLauncher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import li.songe.gkd.a11y.topActivityFlow
import li.songe.gkd.a11y.updateSystemDefaultAppId
import li.songe.gkd.a11y.updateTopActivity
import li.songe.gkd.permission.AuthDialog
import li.songe.gkd.permission.updatePermissionState
import li.songe.gkd.service.A11yService
import li.songe.gkd.service.StatusService
import li.songe.gkd.service.fixRestartAutomatorService
import li.songe.gkd.service.updateTopTaskAppId
import li.songe.gkd.shizuku.automationRegisteredExceptionFlow
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.store.storeFlow
import li.songe.gkd.ui.A11YScopeAppListRoute
import li.songe.gkd.ui.A11yEventLogPage
import li.songe.gkd.ui.A11yEventLogRoute
import li.songe.gkd.ui.A11yScopeAppListPage
import li.songe.gkd.ui.AboutPage
import li.songe.gkd.ui.AboutRoute
import li.songe.gkd.ui.ActionLogPage
import li.songe.gkd.ui.ActionLogRoute
import li.songe.gkd.ui.ActivityLogPage
import li.songe.gkd.ui.ActivityLogRoute
import li.songe.gkd.ui.AdvancedPage
import li.songe.gkd.ui.AdvancedPageRoute
import li.songe.gkd.ui.AppConfigPage
import li.songe.gkd.ui.AppConfigRoute
import li.songe.gkd.ui.AppOpsAllowPage
import li.songe.gkd.ui.AppOpsAllowRoute
import li.songe.gkd.ui.AuthA11yPage
import li.songe.gkd.ui.AuthA11yRoute
import li.songe.gkd.ui.BlockA11yAppListPage
import li.songe.gkd.ui.BlockA11yAppListRoute
import li.songe.gkd.ui.EditBlockAppListPage
import li.songe.gkd.ui.EditBlockAppListRoute
import li.songe.gkd.ui.ImagePreviewPage
import li.songe.gkd.ui.ImagePreviewRoute
import li.songe.gkd.ui.SlowGroupPage
import li.songe.gkd.ui.SlowGroupRoute
import li.songe.gkd.ui.SnapshotPage
import li.songe.gkd.ui.SnapshotPageRoute
import li.songe.gkd.ui.SubsAppGroupListPage
import li.songe.gkd.ui.SubsAppGroupListRoute
import li.songe.gkd.ui.SubsAppListPage
import li.songe.gkd.ui.SubsAppListRoute
import li.songe.gkd.ui.SubsCategoryPage
import li.songe.gkd.ui.SubsCategoryRoute
import li.songe.gkd.ui.SubsGlobalGroupExcludePage
import li.songe.gkd.ui.SubsGlobalGroupExcludeRoute
import li.songe.gkd.ui.SubsGlobalGroupListPage
import li.songe.gkd.ui.SubsGlobalGroupListRoute
import li.songe.gkd.ui.UpsertRuleGroupPage
import li.songe.gkd.ui.UpsertRuleGroupRoute
import li.songe.gkd.ui.WebViewPage
import li.songe.gkd.ui.WebViewRoute
import li.songe.gkd.ui.component.BuildDialog
import li.songe.gkd.ui.component.PerfIcon
import li.songe.gkd.ui.component.ShareDataDialog
import li.songe.gkd.ui.component.SubsSheet
import li.songe.gkd.ui.component.TermsAcceptDialog
import li.songe.gkd.ui.component.TextDialog
import li.songe.gkd.ui.home.HomePage
import li.songe.gkd.ui.home.HomeRoute
import li.songe.gkd.ui.share.FixedWindowInsets
import li.songe.gkd.ui.share.LocalMainViewModel
import li.songe.gkd.ui.style.AppTheme
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.BarUtils
import li.songe.gkd.util.EditGithubCookieDlg
import li.songe.gkd.util.KeyboardUtils
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.ShortUrlSet
import li.songe.gkd.util.appInfoMapFlow
import li.songe.gkd.util.componentName
import li.songe.gkd.util.copyText
import li.songe.gkd.util.fixSomeProblems
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.mapState
import li.songe.gkd.util.openApp
import li.songe.gkd.util.openUri
import li.songe.gkd.util.shizukuAppId
import li.songe.gkd.util.throttle
import li.songe.gkd.util.toast
import kotlin.concurrent.Volatile
import kotlin.reflect.jvm.jvmName
class MainActivity : ComponentActivity() {
val startTime = System.currentTimeMillis()
val mainVm by viewModels()
val launcher by lazy { StartActivityLauncher(this) }
val pickContentLauncher by lazy { PickContentLauncher(this) }
val imeFullHiddenFlow = MutableStateFlow(true)
val imePlayingFlow = MutableStateFlow(false)
private val imeVisible: Boolean
get() = ViewCompat.getRootWindowInsets(window.decorView)!!
.isVisible(WindowInsetsCompat.Type.ime())
var topBarWindowInsets by mutableStateOf(WindowInsets(top = BarUtils.getStatusBarHeight()))
private fun watchKeyboardVisible() {
if (AndroidTarget.R) {
ViewCompat.setWindowInsetsAnimationCallback(
window.decorView,
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
override fun onStart(
animation: WindowInsetsAnimationCompat,
bounds: WindowInsetsAnimationCompat.BoundsCompat
): WindowInsetsAnimationCompat.BoundsCompat {
imePlayingFlow.update { imeVisible }
return super.onStart(animation, bounds)
}
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: List
): WindowInsetsCompat {
return insets
}
override fun onEnd(animation: WindowInsetsAnimationCompat) {
imeFullHiddenFlow.update { !imeVisible }
imePlayingFlow.update { false }
super.onEnd(animation)
}
})
} else {
KeyboardUtils.registerSoftInputChangedListener(window) { height ->
// onEnd
imeFullHiddenFlow.update { height == 0 }
}
}
}
suspend fun hideSoftInput(): Boolean {
if (!imeFullHiddenFlow.updateAndGet { !imeVisible }) {
KeyboardUtils.hideSoftInput(this@MainActivity)
imeFullHiddenFlow.drop(1).first()
return true
}
return false
}
fun justHideSoftInput(): Boolean {
if (!imeFullHiddenFlow.updateAndGet { !imeVisible }) {
KeyboardUtils.hideSoftInput(this@MainActivity)
return true
}
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
enableEdgeToEdge()
fixSomeProblems()
super.onCreate(savedInstanceState)
LogUtils.d()
mainVm
launcher
pickContentLauncher
lifecycleScope.launch {
storeFlow.mapState(lifecycleScope) { s -> s.excludeFromRecents }.collect {
app.activityManager.appTasks.forEach { task ->
task.setExcludeFromRecents(it)
}
}
}
addOnNewIntentListener {
mainVm.handleIntent(it)
intent = null
}
watchKeyboardVisible()
StatusService.autoStart()
if (storeFlow.value.enableBlockA11yAppList) {
updateTopTaskAppId(META.appId)
}
setContent {
val latestInsets = TopAppBarDefaults.windowInsets
val density = LocalDensity.current
if (latestInsets.getTop(density) > topBarWindowInsets.getTop(density)) {
topBarWindowInsets = FixedWindowInsets(latestInsets)
}
CompositionLocalProvider(
LocalMainViewModel provides mainVm
) {
AppTheme {
NavDisplay(
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
backStack = mainVm.backStack,
onBack = mainVm::popPage,
entryProvider = entryProvider {
entry { HomePage() }
entry { AuthA11yPage() }
entry { AboutPage() }
entry { BlockA11yAppListPage() }
entry { AdvancedPage() }
entry { SnapshotPage() }
entry { AppOpsAllowPage() }
entry { A11yScopeAppListPage() }
entry { ActivityLogPage() }
entry { A11yEventLogPage() }
entry { EditBlockAppListPage() }
entry { SlowGroupPage() }
entry { SubsAppListPage(it) }
entry { WebViewPage(it) }
entry { SubsCategoryPage(it) }
entry { SubsGlobalGroupListPage(it) }
entry { SubsGlobalGroupExcludePage(it) }
entry { ActionLogPage(it) }
entry { ImagePreviewPage(it) }
entry { UpsertRuleGroupPage(it) }
entry { SubsAppGroupListPage(it) }
entry { AppConfigPage(it) }
},
transitionSpec = {
slideInHorizontally(initialOffsetX = { it }) togetherWith
slideOutHorizontally(targetOffsetX = { -it })
},
popTransitionSpec = {
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
},
predictivePopTransitionSpec = {
slideInHorizontally(initialOffsetX = { -it }) togetherWith
slideOutHorizontally(targetOffsetX = { it })
},
)
if (!mainVm.termsAcceptedFlow.collectAsState().value) {
TermsAcceptDialog()
} else {
UiAutomationAlreadyRegisteredDlg()
AccessRestrictedSettingsDlg()
ShizukuErrorDialog(mainVm.shizukuErrorFlow)
AuthDialog(mainVm.authReasonFlow)
BuildDialog(mainVm.dialogFlow)
mainVm.uploadOptions.ShowDialog()
EditGithubCookieDlg()
mainVm.updateStatus?.UpgradeDialog()
SubsSheet(mainVm, mainVm.sheetSubsIdFlow)
ShareDataDialog(mainVm, mainVm.showShareDataIdsFlow)
mainVm.inputSubsLinkOption.ContentDialog()
mainVm.ruleGroupState.Render()
TextDialog(mainVm.textFlow)
}
}
}
LaunchedEffect(null) {
intent?.let {
mainVm.handleIntent(it)
intent = null
}
}
}
}
override fun onStart() {
super.onStart()
LogUtils.d()
activityVisibleState++
if (topActivityFlow.value.appId != META.appId) {
updateTopActivity(META.appId, MainActivity::class.jvmName)
}
}
var isFirstResume = true
override fun onResume() {
super.onResume()
LogUtils.d()
if (isFirstResume && startTime - app.startTime < 2000) {
isFirstResume = false
} else {
syncFixState()
}
}
override fun onStop() {
super.onStop()
LogUtils.d()
activityVisibleState--
}
override fun onDestroy() {
super.onDestroy()
LogUtils.d()
}
}
@Volatile
private var activityVisibleState = 0
val isActivityVisible get() = activityVisibleState > 0
val activityNavSourceName by lazy { META.appId + ".activity.nav.source" }
fun Activity.navToMainActivity() {
if (intent != null) {
val navIntent = Intent(intent)
navIntent.component = MainActivity::class.componentName
navIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
navIntent.putExtra(activityNavSourceName, this::class.jvmName)
startActivity(navIntent)
}
finish()
}
private val syncStateMutex = Mutex()
fun syncFixState() {
appScope.launchTry(Dispatchers.IO) {
if (syncStateMutex.isLocked) {
LogUtils.d("syncFixState isLocked")
}
syncStateMutex.withLock {
updateSystemDefaultAppId()
shizukuContextFlow.value.grantSelf()
updatePermissionState()
fixRestartAutomatorService()
}
}
}
@Composable
private fun ShizukuErrorDialog(stateFlow: MutableStateFlow) {
val state = stateFlow.collectAsState().value
if (state != null) {
val errorText = remember { state.stackTraceToString() }
val appInfoCache = appInfoMapFlow.collectAsState().value
val installed = appInfoCache.contains(shizukuAppId)
AlertDialog(
onDismissRequest = { stateFlow.value = null },
title = { Text(text = "授权错误") },
text = {
Column {
Text(
text = if (installed) {
"Shizuku 授权失败,请检查是否运行"
} else {
"Shizuku 授权失败,检测到 Shizuku 未安装,请先下载后安装,如果你是通过其它方式授权,请忽略此提示自行查找原因"
}
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier.fillMaxWidth()
) {
SelectionContainer(
modifier = Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
) {
Text(
text = errorText,
modifier = Modifier
.clip(MaterialTheme.shapes.extraSmall)
.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(8.dp)
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState()),
style = MaterialTheme.typography.bodySmall,
)
}
PerfIcon(
modifier = Modifier
.align(Alignment.TopEnd)
.clickable(onClick = throttle {
copyText(errorText)
})
.padding(4.dp)
.size(20.dp),
imageVector = PerfIcon.ContentCopy,
tint = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.75f),
)
}
}
},
confirmButton = {
if (installed) {
TextButton(onClick = {
stateFlow.value = null
openApp(shizukuAppId)
}) {
Text(text = "打开 Shizuku")
}
} else {
TextButton(onClick = {
stateFlow.value = null
openUri(ShortUrlSet.URL4)
}) {
Text(text = "去下载")
}
}
},
dismissButton = {
TextButton(onClick = { stateFlow.value = null }) {
Text(text = "我知道了")
}
}
)
}
}
val accessRestrictedSettingsShowFlow = MutableStateFlow(false)
@Composable
fun AccessRestrictedSettingsDlg() {
val a11yRunning by A11yService.isRunning.collectAsState()
LaunchedEffect(a11yRunning) {
if (a11yRunning) {
accessRestrictedSettingsShowFlow.value = false
}
}
val accessRestrictedSettingsShow by accessRestrictedSettingsShowFlow.collectAsState()
val mainVm = LocalMainViewModel.current
val isA11yPage = mainVm.topRoute is AuthA11yRoute
LaunchedEffect(isA11yPage, accessRestrictedSettingsShow) {
if (isA11yPage && accessRestrictedSettingsShow && !a11yRunning) {
toast("请重新授权以解除限制")
accessRestrictedSettingsShowFlow.value = false
}
}
if (accessRestrictedSettingsShow && !isA11yPage && !a11yRunning) {
AlertDialog(
title = {
Text(text = "权限受限")
},
text = {
Text(text = "当前操作权限「访问受限设置」已被限制, 请先解除限制")
},
onDismissRequest = {
accessRestrictedSettingsShowFlow.value = false
},
confirmButton = {
TextButton({
accessRestrictedSettingsShowFlow.value = false
mainVm.navigateWebPage(ShortUrlSet.URL2)
}) {
Text(text = "解除")
}
},
dismissButton = {
TextButton({
accessRestrictedSettingsShowFlow.value = false
}) {
Text(text = "关闭")
}
},
)
}
}
@Composable
fun UiAutomationAlreadyRegisteredDlg() {
if (automationRegisteredExceptionFlow.collectAsState().value != null) {
AlertDialog(
onDismissRequest = {
automationRegisteredExceptionFlow.value = null
},
title = { Text(text = "启动失败") },
text = {
Text(text = "自动化服务启动失败,检测到自动化服务已被其他应用占用,请先关闭已有服务后重试\n\n注:自动化服务只能同时运行一个,请确保没有其他应用或测试框架占用后再启动")
},
confirmButton = {
TextButton(onClick = {
automationRegisteredExceptionFlow.value = null
}) {
Text(text = "我知道了")
}
}
)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/MainViewModel.kt
================================================
package li.songe.gkd
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.webkit.URLUtil
import androidx.lifecycle.viewModelScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import li.songe.gkd.a11y.useA11yServiceEnabledFlow
import li.songe.gkd.a11y.useEnabledA11yServicesFlow
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.importData
import li.songe.gkd.db.DbSet
import li.songe.gkd.permission.AuthReason
import li.songe.gkd.permission.shizukuGrantedState
import li.songe.gkd.service.A11yService
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.shizuku.uiAutomationFlow
import li.songe.gkd.shizuku.updateBinderMutex
import li.songe.gkd.store.createTextFlow
import li.songe.gkd.store.storeFlow
import li.songe.gkd.ui.AdvancedPageRoute
import li.songe.gkd.ui.AppOpsAllowRoute
import li.songe.gkd.ui.SnapshotPageRoute
import li.songe.gkd.ui.WebViewRoute
import li.songe.gkd.ui.component.AlertDialogOptions
import li.songe.gkd.ui.component.InputSubsLinkOption
import li.songe.gkd.ui.component.RuleGroupState
import li.songe.gkd.ui.component.UploadOptions
import li.songe.gkd.ui.home.BottomNavItem
import li.songe.gkd.ui.home.HomeRoute
import li.songe.gkd.ui.share.BaseViewModel
import li.songe.gkd.util.AutomatorModeOption
import li.songe.gkd.util.LOCAL_SUBS_ID
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.OnSimpleLife
import li.songe.gkd.util.ThrottleTimer
import li.songe.gkd.util.UpdateStatus
import li.songe.gkd.util.appIconMapFlow
import li.songe.gkd.util.clearCache
import li.songe.gkd.util.client
import li.songe.gkd.util.findOption
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.openUri
import li.songe.gkd.util.openWeChatScaner
import li.songe.gkd.util.runMainPost
import li.songe.gkd.util.stopCoroutine
import li.songe.gkd.util.subsFolder
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubsMutex
import li.songe.gkd.util.updateSubscription
import rikka.shizuku.Shizuku
import kotlin.reflect.jvm.jvmName
class MainViewModel : BaseViewModel(), OnSimpleLife {
companion object {
private var _instance: MainViewModel? = null
val instance get() = _instance!!
private var tempTermsAccepted = false
}
init {
_instance = this
addCloseable {
if (_instance == this) { // 可能同时存在 2 个 MainViewModel 实例
_instance = null
}
}
}
override val scope get() = viewModelScope
val backStack: NavBackStack = NavBackStack(HomeRoute)
val topRoute get() = backStack.last()
private val backThrottleTimer = ThrottleTimer()
fun popPage() = runMainPost {
if (backThrottleTimer.expired() && backStack.size > 1) {
backStack.removeAt(backStack.lastIndex)
}
}
fun navigatePage(navKey: NavKey, replaced: Boolean = false) = runMainPost {
if (navKey != backStack.last()) {
if (replaced) {
backStack[backStack.lastIndex] = navKey
} else {
backStack.add(navKey)
}
}
}
fun navigateWebPage(url: String) = navigatePage(WebViewRoute(url))
val dialogFlow = MutableStateFlow(null)
val authReasonFlow = MutableStateFlow(null)
val updateStatus = if (META.updateEnabled) UpdateStatus(viewModelScope) else null
val shizukuErrorFlow = MutableStateFlow(null)
val uploadOptions = UploadOptions(this)
val showEditCookieDlgFlow = MutableStateFlow(false)
val inputSubsLinkOption = InputSubsLinkOption()
val sheetSubsIdFlow = MutableStateFlow(null)
val showShareDataIdsFlow = MutableStateFlow?>(null)
val appOrderListFlow = DbSet.actionLogDao.queryLatestUniqueAppIds().stateInit(emptyList())
val appVisitOrderMapFlow = DbSet.appVisitLogDao.query().map {
it.mapIndexed { i, appId -> appId to i }.toMap()
}.debounce(500).stateInit(emptyMap())
fun addOrModifySubs(
url: String,
oldItem: SubsItem? = null,
) = viewModelScope.launchTry(Dispatchers.IO) {
if (updateSubsMutex.mutex.isLocked) return@launchTry
updateSubsMutex.withStateLock {
val subItems = subsItemsFlow.value
val text = try {
client.get(url).bodyAsText()
} catch (e: Exception) {
e.printStackTrace()
LogUtils.d(e)
toast("下载订阅文件失败\n${e.message}".trimEnd())
return@launchTry
}
val newSubsRaw = try {
RawSubscription.parse(text)
} catch (e: Exception) {
e.printStackTrace()
LogUtils.d(e)
toast("解析订阅文件失败\n${e.message}".trimEnd())
return@launchTry
}
if (oldItem == null) {
if (subItems.any { it.id == newSubsRaw.id }) {
toast("订阅已存在")
return@launchTry
}
} else {
if (oldItem.id != newSubsRaw.id) {
toast("订阅id不对应")
return@launchTry
}
}
if (newSubsRaw.id < 0) {
toast("订阅id不可为${newSubsRaw.id}\n负数id为内部使用")
return@launchTry
}
val newItem = oldItem?.copy(updateUrl = url) ?: SubsItem(
id = newSubsRaw.id,
updateUrl = url,
order = if (subItems.isEmpty()) 1 else (subItems.maxBy { it.order }.order + 1)
)
updateSubscription(newSubsRaw)
if (oldItem == null) {
DbSet.subsItemDao.insert(newItem)
toast("成功添加订阅")
} else {
DbSet.subsItemDao.update(newItem)
toast("成功修改订阅")
}
}
}
val ruleGroupState = RuleGroupState(this)
val textFlow = MutableStateFlow(null)
fun openUrl(url: String) {
if (URLUtil.isNetworkUrl(url)) {
textFlow.value = url
} else {
openUri(url)
}
}
val tabFlow = MutableStateFlow(BottomNavItem.Control.key)
val resetPageScrollEvent = MutableSharedFlow()
private var lastClickTabTime = 0L
fun handleClickTab(navItem: BottomNavItem) {
val t = System.currentTimeMillis()
// double click
if (navItem.key == tabFlow.value && t - lastClickTabTime < 500) {
viewModelScope.launch { resetPageScrollEvent.emit(navItem) }
}
tabFlow.value = navItem.key
lastClickTabTime = t
}
fun handleGkdUri(uri: Uri) {
val notFoundToast = { toast("未知URI\n${uri}") }
when (uri.host) {
"page" -> when (uri.path) {
"" -> {
val tab = uri.getQueryParameter("tab")?.toIntOrNull()
if (tab != null && BottomNavItem.allSubObjects.any { it.key == tab }) {
tabFlow.value = tab
}
}
"/1" -> navigatePage(AdvancedPageRoute)
"/2" -> navigatePage(SnapshotPageRoute)
"/3" -> navigatePage(AppOpsAllowRoute)
else -> notFoundToast()
}
"invoke" -> when (uri.path) {
"/1" -> openWeChatScaner()
else -> notFoundToast()
}
else -> notFoundToast()
}
}
fun handleIntent(intent: Intent) = viewModelScope.launchTry {
LogUtils.d(intent)
val uri = intent.data?.normalizeScheme()
val source = intent.getStringExtra(activityNavSourceName)
if (uri?.scheme == "gkd") {
handleGkdUri(uri)
} else if (source == OpenFileActivity::class.jvmName && uri != null) {
toast("加载导入中...")
tabFlow.value = BottomNavItem.SubsManage.key
withContext(Dispatchers.IO) { importData(uri) }
}
}
val termsAcceptedFlow by lazy {
if (tempTermsAccepted) {
MutableStateFlow(true)
} else {
createTextFlow(
key = "terms_accepted",
decode = { it == "true" },
encode = {
tempTermsAccepted = it
it.toString()
},
scope = viewModelScope,
).apply {
tempTermsAccepted = value
}
}
}
val githubCookieFlow by lazy {
createTextFlow(
key = "github_cookie",
decode = { it ?: "" },
encode = { it },
private = true,
scope = viewModelScope,
)
}
fun switchEnableShizuku(value: Boolean) {
if (updateBinderMutex.mutex.isLocked) {
toast("正在连接中,请稍后")
return
}
storeFlow.update { s -> s.copy(enableShizuku = value) }
}
fun requestShizuku() {
if (shizukuContextFlow.value.ok) return
if (updateBinderMutex.mutex.isLocked) {
toast("正在连接中,请稍后")
return
}
try {
Shizuku.requestPermission(Activity.RESULT_OK)
} catch (e: Throwable) {
shizukuErrorFlow.value = e
}
}
suspend fun guardShizukuContext() {
if (shizukuContextFlow.value.ok) return
if (!storeFlow.value.enableShizuku) {
storeFlow.update { it.copy(enableShizuku = true) }
}
if (!shizukuGrantedState.updateAndGet()) {
requestShizuku()
stopCoroutine()
}
if (shizukuContextFlow.value.ok) return
stopCoroutine()
}
private val a11yServicesFlow = useEnabledA11yServicesFlow()
val a11yServiceEnabledFlow = useA11yServiceEnabledFlow(a11yServicesFlow)
val hasOtherA11yFlow = a11yServicesFlow.mapNew { list ->
list.any { it != A11yService.a11yCn }
}
val automatorModeFlow = storeFlow.mapNew {
AutomatorModeOption.objects.findOption(it.automatorMode)
}
fun updateAutomatorMode(option: AutomatorModeOption) {
if (automatorModeFlow.value == option) return
storeFlow.update { it.copy(automatorMode = option.value, enableAutomator = false) }
A11yService.instance?.shutdown()
uiAutomationFlow.value?.shutdown()
}
init {
// preload
appIconMapFlow.value
viewModelScope.launchTry(Dispatchers.IO) {
val subsItems = DbSet.subsItemDao.queryAll()
if (!subsItems.any { s -> s.id == LOCAL_SUBS_ID }) {
if (!subsFolder.resolve("${LOCAL_SUBS_ID}.json").exists()) {
updateSubscription(
RawSubscription(
id = LOCAL_SUBS_ID,
name = "本地订阅",
version = 0
)
)
}
DbSet.subsItemDao.insert(
SubsItem(
id = LOCAL_SUBS_ID,
order = subsItems.minByOrNull { it.order }?.order ?: 0,
)
)
}
}
viewModelScope.launchTry(Dispatchers.IO) {
// 每次进入删除缓存
clearCache()
}
if (updateStatus != null && termsAcceptedFlow.value) {
updateStatus.checkUpdate()
}
viewModelScope.launch(Dispatchers.IO) {
// preload
githubCookieFlow.value
}
// for OnSimpleLife
onCreated()
addCloseable { onDestroyed() }
toast("MainViewModel:init")
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/OpenFileActivity.kt
================================================
package li.songe.gkd
import android.app.Activity
import android.os.Bundle
class OpenFileActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navToMainActivity()
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/OpenSchemeActivity.kt
================================================
package li.songe.gkd
import android.app.Activity
import android.os.Bundle
class OpenSchemeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navToMainActivity()
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/OpenTileActivity.kt
================================================
package li.songe.gkd
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.core.net.toUri
import li.songe.gkd.util.extraCptName
class OpenTileActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val qsTileCpt = intent?.extraCptName
if (qsTileCpt != null && intent.data == null) {
val serviceInfo =
app.packageManager.getServiceInfo(qsTileCpt, PackageManager.GET_META_DATA)
val uriValue = serviceInfo.metaData.getString("QS_TILE_URI")
if (uriValue != null) {
intent.data = uriValue.toUri()
}
}
navToMainActivity()
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/a11y/A11yCommonImpl.kt
================================================
package li.songe.gkd.a11y
import android.graphics.Bitmap
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import kotlinx.coroutines.CoroutineScope
import li.songe.gkd.util.AutomatorModeOption
interface A11yCommonImpl {
suspend fun screenshot(): Bitmap?
val windowNodeInfo: AccessibilityNodeInfo?
val windowInfos: List
val scope: CoroutineScope
var justStarted: Boolean
val mode: AutomatorModeOption
val ruleEngine: A11yRuleEngine
fun shutdown(temp: Boolean = false)
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/a11y/A11yContext.kt
================================================
package li.songe.gkd.a11y
import android.util.Log
import android.util.LruCache
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.atomicfu.atomic
import li.songe.gkd.META
import li.songe.gkd.data.ResolvedRule
import li.songe.gkd.shizuku.casted
import li.songe.gkd.util.InterruptRuleMatchException
import li.songe.selector.FastQuery
import li.songe.selector.MatchOption
import li.songe.selector.QueryContext
import li.songe.selector.Selector
import li.songe.selector.Transform
import li.songe.selector.getBooleanInvoke
import li.songe.selector.getCharSequenceAttr
import li.songe.selector.getCharSequenceInvoke
import li.songe.selector.getIntInvoke
private operator fun LruCache.set(child: K, value: V): V {
return put(child, value)
}
private fun List.getInt(i: Int = 0) = get(i) as Int
private const val MAX_CACHE_SIZE = MAX_DESCENDANTS_SIZE
private val AccessibilityNodeInfo?.notExpiredNode: AccessibilityNodeInfo?
get() {
if (this != null) {
val expiryMillis = if (text == null) 2000L else 1000L
if (isExpired(expiryMillis)) {
return null
}
}
return this
}
class A11yContext(
private val a11yEngine: A11yRuleEngine,
private val interruptable: Boolean = true,
) {
private var childCache =
LruCache, AccessibilityNodeInfo>(MAX_CACHE_SIZE)
private var indexCache = LruCache(MAX_CACHE_SIZE)
private var parentCache = LruCache(MAX_CACHE_SIZE)
val rootCache = atomic(null)
private fun clearChildCache(node: AccessibilityNodeInfo) {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i ->
childCache.remove(node to i)?.let {
clearChildCache(it)
}
}
}
fun clearNodeCache(eventNode: AccessibilityNodeInfo? = null) {
if (rootCache.value?.packageName != topActivityFlow.value.appId) {
rootCache.value = null
}
if (eventNode != null) {
clearChildCache(eventNode)
parentCache[eventNode]?.let { p ->
getPureIndex(eventNode)?.let { i ->
childCache[p to i] = eventNode
}
}
if (rootCache.value == eventNode) {
rootCache.value = eventNode
} else {
if (META.debuggable) {
Log.d(
"cache",
"clear node cache ${eventNode.packageName}/${eventNode.className}"
)
}
return
}
}
if (META.debuggable) {
val sizeList = listOf(childCache.size(), parentCache.size(), indexCache.size())
if (sizeList.any { it > 0 }) {
Log.d("cache", "clear cache -> $sizeList")
}
}
try {
childCache.evictAll()
parentCache.evictAll()
indexCache.evictAll()
} catch (_: Exception) {
// https://github.com/gkd-kit/gkd/issues/664
// 在某些机型上 未知原因 缓存不一致 导致删除失败
childCache = LruCache(MAX_CACHE_SIZE)
indexCache = LruCache(MAX_CACHE_SIZE)
parentCache = LruCache(MAX_CACHE_SIZE)
}
}
private var lastAppChangeTime = appChangeTime
fun clearOldAppNodeCache(): Boolean {
if (appChangeTime != lastAppChangeTime) {
lastAppChangeTime = appChangeTime
clearNodeCache()
return true
}
return false
}
var currentRule: ResolvedRule? = null
@Volatile
var interruptKey = 0
private var interruptInnerKey = 0
private fun guardInterrupt() {
if (!interruptable) return
if (interruptInnerKey == interruptKey) return
interruptInnerKey = interruptKey
val rule = currentRule ?: return
if (!activityRuleFlow.value.activePriority) return
if (!activityRuleFlow.value.currentRules.any { it === rule }) return
if (rule.isPriority()) return
if (META.debuggable) {
Log.d("guardInterrupt", "中断 rule=${rule.statusText()}")
}
throw InterruptRuleMatchException()
}
private fun getA11Root(): AccessibilityNodeInfo? {
guardInterrupt()
return a11yEngine.safeActiveWindow
}
private fun getA11Child(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? {
guardInterrupt()
return node.getChild(index)?.setGeneratedTime()
}
private fun getA11Parent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
guardInterrupt()
return node.parent?.setGeneratedTime()
}
private fun getA11ByText(
node: AccessibilityNodeInfo,
value: String
): List {
guardInterrupt()
return node.findAccessibilityNodeInfosByText(value).apply {
forEach { it.setGeneratedTime() }
}
}
private fun getA11ById(
node: AccessibilityNodeInfo,
value: String
): List {
guardInterrupt()
return node.findAccessibilityNodeInfosByViewId(value).apply {
forEach { it.setGeneratedTime() }
}
}
private fun getFastQueryNodes(
node: AccessibilityNodeInfo,
fastQuery: FastQuery
): List {
return when (fastQuery) {
is FastQuery.Id -> getA11ById(node, fastQuery.value)
is FastQuery.Text -> getA11ByText(node, fastQuery.value)
is FastQuery.Vid -> getA11ById(node, "${node.packageName}:id/${fastQuery.value}")
}
}
private fun getCacheRoot(node: AccessibilityNodeInfo? = null): AccessibilityNodeInfo? {
if (rootCache.value.notExpiredNode == null) {
rootCache.value = getA11Root()
}
if (node == rootCache.value) return null
return rootCache.value
}
private fun getCacheParent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (getCacheRoot() == node) {
return null
}
parentCache[node].notExpiredNode?.let { return it }
return getA11Parent(node).apply {
if (this != null) {
parentCache[node] = this
} else {
rootCache.value = node
}
}
}
private fun getCacheChild(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? {
if (index !in 0 until node.childCount) {
return null
}
return childCache[node to index].notExpiredNode ?: getA11Child(node, index)?.also { child ->
indexCache[child] = index
parentCache[child] = node
childCache[node to index] = child
}
}
private fun getPureIndex(node: AccessibilityNodeInfo): Int? {
return indexCache[node]
}
private fun getCacheIndex(node: AccessibilityNodeInfo): Int {
indexCache[node]?.let { return it }
getCacheChildren(getCacheParent(node)).forEachIndexed { index, child ->
if (child == node) {
indexCache[node] = index
return index
}
}
return 0
}
/**
* 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知
*
* https://github.com/gkd-kit/gkd/issues/28
*/
private fun getCacheDepth(node: AccessibilityNodeInfo): Int {
var p: AccessibilityNodeInfo = node
var depth = 0
while (true) {
val p2 = getCacheParent(p)
if (p2 != null) {
p = p2
depth++
} else {
break
}
}
return depth
}
private fun getCacheChildren(node: AccessibilityNodeInfo?): Sequence {
if (node == null) return emptySequence()
return sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index ->
val child = getCacheChild(node, index) ?: return@sequence
yield(child)
}
}
}
private var tempVid: CharSequence? = null
private var tempVidNode: AccessibilityNodeInfo? = null
private fun getTempVid(n: AccessibilityNodeInfo): CharSequence? {
if (n !== tempVidNode) {
tempVid = n.getVid()
tempVidNode = n
}
return tempVid
}
private fun getCacheAttr(node: AccessibilityNodeInfo, name: String): Any? = when (name) {
"id" -> node.viewIdResourceName
"vid" -> getTempVid(node)
"name" -> node.className
"text" -> node.text
"desc" -> node.contentDescription
"clickable" -> node.isClickable
"focusable" -> node.isFocusable
"checkable" -> node.isCheckable
"checked" -> node.compatChecked
"editable" -> node.isEditable
"longClickable" -> node.isLongClickable
"visibleToUser" -> node.isVisibleToUser
"left" -> node.casted.boundsInScreen.left
"top" -> node.casted.boundsInScreen.top
"right" -> node.casted.boundsInScreen.right
"bottom" -> node.casted.boundsInScreen.bottom
"width" -> node.casted.boundsInScreen.width()
"height" -> node.casted.boundsInScreen.height()
"index" -> getCacheIndex(node)
"depth" -> getCacheDepth(node)
"childCount" -> node.childCount
"parent" -> getCacheParent(node)
else -> null
}
private val transform = Transform(
getAttr = { target, name ->
when (target) {
is QueryContext<*> -> when (name) {
"prev" -> target.prev
"current" -> target.current
else -> getCacheAttr(target.current as AccessibilityNodeInfo, name)
}
is AccessibilityNodeInfo -> getCacheAttr(target, name)
is CharSequence -> getCharSequenceAttr(target, name)
else -> null
}
},
getInvoke = { target, name, args ->
when (target) {
is AccessibilityNodeInfo -> when (name) {
"getChild" -> {
getCacheChild(target, args.getInt())
}
else -> null
}
is QueryContext<*> -> when (name) {
"getPrev" -> {
args.getInt().let { target.getPrev(it) }
}
"getChild" -> {
getCacheChild(target.current as AccessibilityNodeInfo, args.getInt())
}
else -> null
}
is CharSequence -> getCharSequenceInvoke(target, name, args)
is Int -> getIntInvoke(target, name, args)
is Boolean -> getBooleanInvoke(target, name, args)
else -> null
}
},
getName = { node -> node.className },
getChildren = ::getCacheChildren,
getParent = ::getCacheParent,
getRoot = ::getCacheRoot,
getDescendants = { node ->
sequence {
val stack = getCacheChildren(node).toMutableList()
if (stack.isEmpty()) return@sequence
stack.reverse()
val tempNodes = mutableListOf()
do {
val top = stack.removeAt(stack.lastIndex)
yield(top)
for (childNode in getCacheChildren(top)) {
tempNodes.add(childNode)
}
if (tempNodes.isNotEmpty()) {
for (i in tempNodes.size - 1 downTo 0) {
stack.add(tempNodes[i])
}
tempNodes.clear()
}
} while (stack.isNotEmpty())
}.take(MAX_DESCENDANTS_SIZE)
},
traverseChildren = { node, connectExpression ->
sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset ->
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = getCacheChild(node, offset) ?: return@sequence
yield(child)
}
}
}
},
traverseBeforeBrothers = { node, connectExpression ->
sequence {
val parentVal = getCacheParent(node) ?: return@sequence
// 如果 node 由 fastQuery 得到, 则第一次调用此方法可能得到 cache.index 是空
val index = getPureIndex(node)
if (index != null) {
var i = index - 1
var offset = 0
while (0 <= i && i < parentVal.childCount) {
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = getCacheChild(parentVal, i) ?: return@sequence
yield(child)
}
i--
offset++
}
} else {
val list = getCacheChildren(parentVal).takeWhile { it != node }.toMutableList()
list.reverse()
yieldAll(list.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
})
}
}
},
traverseAfterBrothers = { node, connectExpression ->
val parentVal = getCacheParent(node)
if (parentVal != null) {
val index = getPureIndex(node)
if (index != null) {
sequence {
var i = index + 1
var offset = 0
while (0 <= i && i < parentVal.childCount) {
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = getCacheChild(parentVal, i) ?: return@sequence
yield(child)
}
i++
offset++
}
}
} else {
getCacheChildren(parentVal).dropWhile { it != node }
.drop(1)
.let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}
.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
}
} else {
emptySequence()
}
},
traverseDescendants = { node, connectExpression ->
sequence {
val stack = getCacheChildren(node).toMutableList()
if (stack.isEmpty()) return@sequence
stack.reverse()
val tempNodes = mutableListOf()
var offset = 0
do {
val top = stack.removeAt(stack.lastIndex)
if (connectExpression.checkOffset(offset)) {
yield(top)
}
offset++
if (offset > MAX_DESCENDANTS_SIZE) {
return@sequence
}
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
for (childNode in getCacheChildren(top)) {
tempNodes.add(childNode)
}
if (tempNodes.isNotEmpty()) {
for (i in tempNodes.size - 1 downTo 0) {
stack.add(tempNodes[i])
}
tempNodes.clear()
}
} while (stack.isNotEmpty())
}
},
traverseFastQueryDescendants = { node, list ->
sequence {
for (fastQuery in list) {
val nodes = getFastQueryNodes(node, fastQuery)
nodes.forEach { childNode ->
yield(childNode)
}
}
}
}
)
fun querySelfOrSelector(
node: AccessibilityNodeInfo,
selector: Selector,
option: MatchOption,
): AccessibilityNodeInfo? {
if (selector.isMatchRoot) {
return selector.match(
getCacheRoot() ?: return null,
transform,
option
)
}
selector.match(node, transform, option)?.let {
return it
}
return transform.querySelector(node, selector, option)
}
fun queryRule(
rule: ResolvedRule,
node: AccessibilityNodeInfo,
): AccessibilityNodeInfo? {
currentRule = rule
try {
val queryNode = if (rule.matchRoot) {
getCacheRoot()
} else {
node
} ?: return null
var resultNode: AccessibilityNodeInfo? = null
if (rule.anyMatches.isNotEmpty()) {
for (selector in rule.anyMatches) {
resultNode = querySelfOrSelector(
queryNode,
selector,
rule.matchOption,
)
if (resultNode != null) break
}
if (resultNode == null) return null
}
for (selector in rule.matches) {
resultNode = querySelfOrSelector(
queryNode,
selector,
rule.matchOption,
) ?: return null
}
for (selector in rule.excludeMatches) {
querySelfOrSelector(
queryNode,
selector,
rule.matchOption,
)?.let { return null }
}
if (rule.excludeAllMatches.isNotEmpty()) {
val allExclude = rule.excludeAllMatches.all {
querySelfOrSelector(
queryNode,
it,
rule.matchOption,
) == null
}
if (!allExclude) {
return null
}
}
return resultNode
} finally {
currentRule = null
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/a11y/A11yExt.kt
================================================
package li.songe.gkd.a11y
import android.content.ComponentName
import android.provider.Settings
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import li.songe.gkd.app
import li.songe.gkd.contentObserver
import li.songe.gkd.service.A11yService
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.OnSimpleLife
import li.songe.gkd.util.mapState
import li.songe.selector.initDefaultTypeInfo
import kotlin.contracts.contract
context(context: OnSimpleLife)
fun useEnabledA11yServicesFlow(): StateFlow> {
val stateFlow = MutableStateFlow(app.getSecureA11yServices())
val contextObserver = contentObserver {
stateFlow.value = app.getSecureA11yServices()
}
app.registerObserver(
Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES),
contextObserver
)
context.onDestroyed {
app.unregisterObserver(contextObserver)
}
return stateFlow
}
context(context: OnSimpleLife)
fun useA11yServiceEnabledFlow(servicesFlow: StateFlow> = useEnabledA11yServicesFlow()): StateFlow {
return servicesFlow.mapState(context.scope) {
it.contains(A11yService.a11yCn)
}
}
const val STATE_CHANGED = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
const val CONTENT_CHANGED = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
// 某些应用耗时 300ms
private val AccessibilityEvent.safeSource: AccessibilityNodeInfo?
get() = if (className == null) {
null // https://github.com/gkd-kit/gkd/issues/426 event.clear 已被系统调用
} else {
try {
source?.setGeneratedTime()
} catch (_: Exception) {
// 原因未知, 仍然报错 Cannot perform this action on a not sealed instance.
null
}
}
fun AccessibilityNodeInfo.getVid(): CharSequence? {
val id = viewIdResourceName ?: return null
val appId = packageName ?: return null
if (id.startsWith(appId) && id.startsWith(":id/", appId.length)) {
return id.subSequence(
appId.length + ":id/".length,
id.length
)
}
return null
}
// https://github.com/gkd-kit/gkd/issues/115
// https://github.com/gkd-kit/gkd/issues/650
// 限制节点遍历的数量避免内存溢出
const val MAX_CHILD_SIZE = 512
const val MAX_DESCENDANTS_SIZE = 4096
private const val A11Y_NODE_TIME_KEY = "generatedTime"
fun AccessibilityNodeInfo.setGeneratedTime(): AccessibilityNodeInfo {
extras.putLong(A11Y_NODE_TIME_KEY, System.currentTimeMillis())
return this
}
fun AccessibilityNodeInfo.isExpired(expiryMillis: Long): Boolean {
val generatedTime = extras.getLong(A11Y_NODE_TIME_KEY, -1)
if (generatedTime == -1L) {
// https://github.com/gkd-kit/gkd/issues/759
return true
}
return (System.currentTimeMillis() - generatedTime) > expiryMillis
}
val typeInfo by lazy { initDefaultTypeInfo().globalType }
val AccessibilityNodeInfo.compatChecked: Boolean?
get() = if (AndroidTarget.BAKLAVA) {
when (checked) {
AccessibilityNodeInfo.CHECKED_STATE_TRUE -> true
AccessibilityNodeInfo.CHECKED_STATE_FALSE -> false
AccessibilityNodeInfo.CHECKED_STATE_PARTIAL -> null
else -> null
}
} else {
@Suppress("DEPRECATION")
isChecked
}
private const val interestedEvents = STATE_CHANGED or CONTENT_CHANGED
fun AccessibilityEvent?.isUseful(): Boolean {
contract {
returns(true) implies (this@isUseful != null)
}
return (this != null && packageName != null && className != null && eventType and interestedEvents != 0)
}
data class A11yEvent(
val type: Int,
val time: Long,
val appId: String,
val name: String,
val event: AccessibilityEvent,
) {
val safeSource: AccessibilityNodeInfo?
get() = event.safeSource
fun sameAs(other: A11yEvent): Boolean {
if (other === this) return true
return type == other.type && appId == other.appId && name == other.name
}
}
// AccessibilityEvent 的 clear 方法会在后续时间被 某些系统 调用导致内部数据丢失, 导致异步子线程获取到的数据不一致
fun AccessibilityEvent.toA11yEvent(): A11yEvent? {
val appId = packageName ?: return null
val b = className ?: return null
return A11yEvent(
type = eventType,
time = System.currentTimeMillis(),
appId = appId.toString(),
name = b.toString(),
event = this,
)
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/a11y/A11yFeat.kt
================================================
package li.songe.gkd.a11y
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import li.songe.gkd.app
import li.songe.gkd.appScope
import li.songe.gkd.permission.shizukuGrantedState
import li.songe.gkd.store.storeFlow
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.ScreenUtils
import li.songe.gkd.util.SnapshotExt
import li.songe.gkd.util.UpdateTimeOption
import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.mapState
import li.songe.selector.MatchOption
import li.songe.selector.QueryContext
import li.songe.selector.Selector
import li.songe.selector.Transform
import li.songe.selector.getBooleanInvoke
import li.songe.selector.getCharSequenceAttr
import li.songe.selector.getCharSequenceInvoke
import li.songe.selector.getIntInvoke
fun onA11yFeatEvent(event: AccessibilityEvent) = event.run {
if (event.eventType == STATE_CHANGED) {
watchCaptureScreenshot()
if (event.packageName == launcherAppId) {
watchCheckShizukuState()
watchAutoUpdateSubs()
}
}
}
private var lastCheckShizukuTime = 0L
private fun watchCheckShizukuState() {
// 借助无障碍轮询校验 shizuku 权限, 因为 shizuku 可能无故被关闭
if (storeFlow.value.enableShizuku) {
val t = System.currentTimeMillis()
if (t - lastCheckShizukuTime > 60 * 60_000L) {
lastCheckShizukuTime = t
appScope.launchTry(Dispatchers.IO) {
shizukuGrantedState.updateAndGet()
}
}
}
}
private var tempEventSelector = "" to (null as Selector?)
private fun AccessibilityEvent.getEventAttr(name: String): Any? = when (name) {
"name" -> className
"desc" -> contentDescription
"text" -> text
else -> null
}
private val a11yEventTransform by lazy {
Transform(
getAttr = { target, name ->
when (target) {
is QueryContext<*> -> when (name) {
"prev" -> target.prev
"current" -> target.current
else -> (target.current as AccessibilityEvent).getEventAttr(name)
}
is CharSequence -> getCharSequenceAttr(target, name)
is AccessibilityEvent -> target.getEventAttr(name)
is List<*> -> when (name) {
"size" -> target.size
else -> null
}
else -> null
}
},
getInvoke = { target, name, args ->
Log.d("A11yEventTransform", "getInvoke: $name(${args.joinToString()}) on $target")
when (target) {
is Int -> getIntInvoke(target, name, args)
is Boolean -> getBooleanInvoke(target, name, args)
is CharSequence -> getCharSequenceInvoke(target, name, args)
is List<*> -> when (name) {
"get" -> {
(args.singleOrNull() as? Int)?.let { index ->
target.getOrNull(index)
}
}
else -> null
}
else -> null
}
},
getName = { it.className },
getChildren = { emptySequence() },
getParent = { null }
)
}
context(event: AccessibilityEvent)
private fun watchCaptureScreenshot() {
if (!storeFlow.value.captureScreenshot) return
if (event.packageName != storeFlow.value.screenshotTargetAppId) return
if (tempEventSelector.first != storeFlow.value.screenshotEventSelector) {
tempEventSelector =
storeFlow.value.screenshotEventSelector to Selector.parseOrNull(storeFlow.value.screenshotEventSelector)
}
val selector = tempEventSelector.second ?: return
selector.match(event, a11yEventTransform, MatchOption(fastQuery = false)).let {
if (it == null) return
}
appScope.launchTry {
SnapshotExt.captureSnapshot(skipScreenshot = true)
}
}
private var lastUpdateSubsTime = 0L
private fun watchAutoUpdateSubs() {
val i = storeFlow.value.updateSubsInterval
if (i <= 0) return
val t = System.currentTimeMillis()
if (t - lastUpdateSubsTime > i.coerceAtLeast(UpdateTimeOption.Everyday.value)) {
lastUpdateSubsTime = t
checkSubsUpdate()
}
}
private fun initRuleChangedLog() {
appScope.launch(Dispatchers.Default) {
activityRuleFlow.debounce(300).drop(1).collect {
if (storeFlow.value.enableMatch && it.currentRules.isNotEmpty()) {
LogUtils.d(it.topActivity, *it.currentRules.map { r ->
r.statusText()
}.toTypedArray())
}
}
}
}
private const val volumeChangedAction = "android.media.VOLUME_CHANGED_ACTION"
private fun createVolumeReceiver() = object : BroadcastReceiver() {
var lastVolumeTriggerTime = -1L
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == volumeChangedAction) {
val t = System.currentTimeMillis()
if (t - lastVolumeTriggerTime > 3000 && !ScreenUtils.isScreenLock()) {
lastVolumeTriggerTime = t
appScope.launchTry {
SnapshotExt.captureSnapshot()
}
}
}
}
}
private fun initCaptureVolume() {
var captureVolumeReceiver: BroadcastReceiver? = null
val changeRegister: (Boolean) -> Unit = {
captureVolumeReceiver?.let(app::unregisterReceiver)
captureVolumeReceiver = if (it) {
createVolumeReceiver().apply {
ContextCompat.registerReceiver(
app,
this,
IntentFilter(volumeChangedAction),
ContextCompat.RECEIVER_EXPORTED
)
}
} else {
null
}
}
appScope.launch(Dispatchers.IO) {
storeFlow.mapState(appScope) { s -> s.captureVolumeChange }.collect(changeRegister)
}
}
var isInteractive = true
private set
private val screenStateReceiver = object : BroadcastReceiver() {
override fun onReceive(
context: Context?,
intent: Intent?
) {
val action = intent?.action ?: return
LogUtils.d("screenStateReceiver->${action}")
isInteractive = when (action) {
Intent.ACTION_SCREEN_ON -> true
Intent.ACTION_SCREEN_OFF -> false
Intent.ACTION_USER_PRESENT -> true
else -> isInteractive
}
if (isInteractive) {
val t = System.currentTimeMillis()
if (t - appChangeTime > 500) { // 37.872(a11y) -> 38.228(onReceive)
A11yRuleEngine.onScreenForcedActive()
}
}
}
}
private fun initScreenStateReceiver() {
isInteractive = app.powerManager.isInteractive
ContextCompat.registerReceiver(
app,
screenStateReceiver,
IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_USER_PRESENT)
},
ContextCompat.RECEIVER_EXPORTED
)
}
fun initA11yFeat() {
initRuleChangedLog()
initCaptureVolume()
initScreenStateReceiver()
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/a11y/A11yRuleEngine.kt
================================================
package li.songe.gkd.a11y
import android.accessibilityservice.AccessibilityService
import android.graphics.Bitmap
import android.util.Log
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.getAndUpdate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import li.songe.gkd.META
import li.songe.gkd.data.ActionPerformer
import li.songe.gkd.data.ActionResult
import li.songe.gkd.data.AppRule
import li.songe.gkd.data.GkdAction
import li.songe.gkd.data.ResolvedRule
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.RuleStatus
import li.songe.gkd.isActivityVisible
import li.songe.gkd.service.A11yService
import li.songe.gkd.service.EventService
import li.songe.gkd.service.topAppIdFlow
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.shizuku.uiAutomationFlow
import li.songe.gkd.store.actualBlockA11yAppList
import li.songe.gkd.store.storeFlow
import li.songe.gkd.util.AutomatorModeOption
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.runMainPost
import li.songe.gkd.util.showActionToast
import li.songe.gkd.util.systemUiAppId
import li.songe.selector.MatchOption
import li.songe.selector.Selector
import java.util.concurrent.Executors
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
private val eventDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val queryDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val actionDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val latestServiceMode = atomic(0)
private val latestServiceTime = atomic(0L)
class A11yRuleEngine(val service: A11yCommonImpl) {
private val a11yContext = A11yContext(this)
private val effective get() = latestServiceMode.value == service.mode.value
private val hasOthersService = when (service.mode) {
AutomatorModeOption.A11yMode -> uiAutomationFlow.value != null
AutomatorModeOption.AutomationMode -> A11yService.instance != null
}
fun onA11yConnected() {
val serviceTime = System.currentTimeMillis()
latestServiceMode.value = service.mode.value
latestServiceTime.value = serviceTime
if (storeFlow.value.enableBlockA11yAppList && !actualBlockA11yAppList.contains(topAppIdFlow.value)) {
startQueryJob(byForced = true)
}
runMainPost(1000L) {// 共存 1000ms, 等待另一个服务稳定
if (latestServiceTime.value == serviceTime) {
when (service.mode) {
AutomatorModeOption.A11yMode -> uiAutomationFlow.value?.shutdown(true)
AutomatorModeOption.AutomationMode -> A11yService.instance?.shutdown(true)
}
}
}
}
fun onScreenForcedActive() {
// 关闭屏幕 -> Activity::onStop -> 点亮屏幕 -> Activity::onStart -> Activity::onResume
val a = topActivityFlow.value
updateTopActivity(a.appId, a.activityId, scene = ActivityScene.ScreenOn)
startQueryJob()
}
val safeActiveWindow: AccessibilityNodeInfo?
get() = try {
// 某些应用耗时 554ms
// java.lang.SecurityException: Call from user 0 as user -2 without permission INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL not allowed.
service.windowNodeInfo?.setGeneratedTime()
} catch (_: Throwable) {
null
}.apply {
a11yContext.rootCache.value = this
}
val safeActiveWindowAppId: String?
get() = safeActiveWindow?.packageName?.toString()
private val scope get() = service.scope
private var lastContentEventTime = 0L
private var lastEventTime = 0L
private val eventDeque = ArrayDeque()
fun onA11yEvent(event: AccessibilityEvent?) {
if (!effective) return
if (!event.isUseful()) return
onA11yFeatEvent(event)
if (event.eventType == CONTENT_CHANGED) {
if (!isInteractive) return // 屏幕关闭后仍然有无障碍事件 type:2048, time:8094, app:com.miui.aod, cls:android.widget.TextView
if (event.packageName == systemUiAppId && event.packageName != topActivityFlow.value.appId) return
}
// 过滤部分输入法事件
if (event.packageName == imeAppId && topActivityFlow.value.appId != imeAppId) {
if (event.recordCount == 0 && event.action == 0 && !event.isFullScreen) return
}
// 直接丢弃自身事件,自行更新 topActivity
if ((event.eventType == CONTENT_CHANGED || !isActivityVisible) && event.packageName == META.appId) return
val a11yEvent = event.toA11yEvent() ?: return
if (a11yEvent.type == CONTENT_CHANGED) {
// 防止 content 类型事件过快
if (a11yEvent.time - lastContentEventTime < 100 && a11yEvent.time - appChangeTime > 5000 && a11yEvent.time - lastTriggerTime > 3000) {
return
}
lastContentEventTime = a11yEvent.time
}
EventService.logEvent(event)
if (META.debuggable) {
Log.d(
"onNewA11yEvent",
"type:${event.eventType}, time:${event.eventTime - lastEventTime}, app:${event.packageName}, cls:${event.className}"
)
}
if (event.eventTime < lastEventTime) {
// 某些应用会发送负时间事件, 直接丢弃
// type:32, time:-104, app:com.miui.home, cls:com.miui.home.launcher.Launcher
return
}
lastEventTime = event.eventTime
synchronized(eventDeque) { eventDeque.addLast(a11yEvent) }
scope.launch(eventDispatcher) { consumeEvent(a11yEvent) }
}
private val queryEvents = mutableListOf()
private suspend fun consumeEvent(headEvent: A11yEvent) {
val consumedEvents = synchronized(eventDeque) {
if (eventDeque.firstOrNull() !== headEvent) return
eventDeque.filter { it.sameAs(headEvent) }.apply {
repeat(size) { eventDeque.removeFirst() }
}
}
val latestEvent = consumedEvents.last()
val evAppId = latestEvent.appId
val evActivityId = latestEvent.name
val oldAppId = topActivityFlow.value.appId
val rightAppId = if (oldAppId == evAppId) {
evAppId
} else {
getTimeoutAppId() ?: return
}
if (rightAppId == evAppId) {
if (latestEvent.type == STATE_CHANGED) {
// tv.danmaku.bili, com.miui.home, com.miui.home.launcher.Launcher
if (isActivity(evAppId, evActivityId)) {
updateTopActivity(evAppId, evActivityId)
}
}
}
if (rightAppId != topActivityFlow.value.appId) {
// 从 锁屏,下拉通知栏 返回等情况, 应用不会发送事件, 但是系统组件会发送事件
val topCpn = shizukuContextFlow.value.topCpn()
if (topCpn?.packageName == rightAppId) {
updateTopActivity(topCpn.packageName, topCpn.className)
} else {
updateTopActivity(rightAppId, null)
}
}
val activityRule = activityRuleFlow.value
if (evAppId != rightAppId || activityRule.skipConsumeEvent || !storeFlow.value.enableMatch) {
return
}
synchronized(queryEvents) { queryEvents.addAll(consumedEvents) }
a11yContext.interruptKey++
startQueryJob(byEvent = latestEvent)
}
private var lastGetAppIdTime = 0L
private var lastAppId: String? = null
private suspend fun getTimeoutAppId(): String? {
if (lastAppId != null && System.currentTimeMillis() - lastGetAppIdTime <= 100) return lastAppId
// 某些应用通过无障碍获取 safeActiveWindow 耗时长,导致多个事件连续堆积堵塞,无法检测到 appId 切换导致状态异常
// https://github.com/gkd-kit/gkd/issues/622
lastAppId = withTimeoutOrNull(100) {
runInterruptible(Dispatchers.IO) { safeActiveWindowAppId }
} ?: shizukuContextFlow.value.topCpn()?.packageName
lastGetAppIdTime = System.currentTimeMillis()
return lastAppId
}
// 某些场景耗时 5000 ms
private suspend fun getTimeoutActiveWindow(): AccessibilityNodeInfo? = suspendCoroutine { s ->
val temp = atomic?>(s)
scope.launch(Dispatchers.IO) {
delay(500L)
temp.getAndUpdate { null }?.resume(null)
}
scope.launch(Dispatchers.IO) {
val a = safeActiveWindow
temp.getAndUpdate { null }?.resume(a)
}
}
@Volatile
private var querying = false
@Synchronized
private fun startQueryJob(
byEvent: A11yEvent? = null,
byForced: Boolean = false,
byDelayRule: ResolvedRule? = null,
) {
if (!effective) return
if (!storeFlow.value.enableMatch) return
if (activityRuleFlow.value.currentRules.isEmpty()) return
if (querying) return
// 无障碍从零启动时获取 safeActiveWindow 非常耗时
if (byEvent == null && service.justStarted && !hasOthersService) return checkFutureStartJob()
scope.launchTry(queryDispatcher) {
querying = true
val st = System.currentTimeMillis()
try {
Log.d(
"A11yRuleEngine",
"startQueryJob start byEvent=${byEvent != null}, byForced=$byForced, byDelayRule=${byDelayRule != null}"
)
queryAction(byEvent, byForced, byDelayRule)
} finally {
checkFutureStartJob()
val et = System.currentTimeMillis() - st
Log.d("A11yRuleEngine", "startQueryJob end $et ms")
querying = false
}
}
}
private fun checkFutureStartJob() {
val t = System.currentTimeMillis()
if (t - lastTriggerTime < 3000L || t - appChangeTime < 3000L) {
scope.launch(actionDispatcher) {
delay(300)
startQueryJob()
}
} else if (activityRuleFlow.value.hasFeatureAction) {
scope.launch(actionDispatcher) {
delay(300)
startQueryJob(byForced = true)
}
}
}
private fun fixAppId(rightAppId: String) {
if (topActivityFlow.value.appId == rightAppId) return
val topCpn = shizukuContextFlow.value.topCpn()
if (topCpn?.packageName == rightAppId) {
updateTopActivity(topCpn.packageName, topCpn.className)
} else {
updateTopActivity(rightAppId, null)
}
scope.launch(actionDispatcher) {
delay(300)
startQueryJob()
}
}
private suspend fun queryAction(
byEvent: A11yEvent? = null,
byForced: Boolean = false,
delayRule: ResolvedRule? = null,
) {
val newEvents = if (delayRule != null) {// 延迟规则不消耗事件
null
} else {
synchronized(queryEvents) {
if (byEvent != null && queryEvents.isEmpty()) {
return
}
(if (queryEvents.size > 1) {
val hasDiffItem = queryEvents.any { e ->
queryEvents.any { e2 -> !e.sameAs(e2) }
}
if (hasDiffItem) {
// 存在不同的事件节点, 全部丢弃使用 root 查询
null
} else {
// type,appId,className 一致, 需要在 synchronized 外验证是否是同一节点
arrayOf(
queryEvents[queryEvents.size - 2],
queryEvents.last(),
)
}
} else if (queryEvents.size == 1) {
arrayOf(queryEvents.last())
} else {
null
}).apply {
queryEvents.clear()
}
}
}
val activityRule = activityRuleFlow.value
activityRule.currentRules.forEach { rule ->
if (rule.status == RuleStatus.Status3 && rule.matchDelayJob.value == null) {
rule.matchDelayJob.value = scope.launch(actionDispatcher) {
delay(rule.matchDelay)
rule.matchDelayJob.value = null
startQueryJob(byDelayRule = rule)
}
}
}
if (activityRule.skipMatch) {
// 如果当前应用没有规则/暂停匹配, 则不去调用获取事件节点避免阻塞
return
}
var lastNode = if (newEvents == null || newEvents.size <= 1) {
newEvents?.firstOrNull()?.safeSource
} else {
// 获取最后两个事件, 如果最后两个事件的节点不一致, 则丢弃
// 相等则是同一个节点发出的连续事件, 常见于倒计时界面
val lastNode = newEvents.last().safeSource
if (lastNode == null || lastNode == newEvents[0].safeSource) {
lastNode
} else {
null
}
}
var lastNodeUsed = false
if (!a11yContext.clearOldAppNodeCache()) {
if (byEvent != null) { // 此为多数情况
// 新事件到来时, 若缓存清理不及时会导致无法查询到节点
a11yContext.clearNodeCache(lastNode)
}
}
for (rule in activityRule.priorityRules) { // 规则数量有可能过多导致耗时过长
if (!effective) return
if (activityRule !== activityRuleFlow.value) break
if (delayRule != null && delayRule !== rule) continue
if (rule.status != RuleStatus.StatusOk) continue
if (byForced && !rule.checkForced()) continue
lastNode?.let { n ->
val refreshOk = (!lastNodeUsed) || (try {
val e = n.refresh()
if (e) {
n.setGeneratedTime()
}
e
} catch (_: Throwable) {
false
})
lastNodeUsed = true
if (!refreshOk) {
lastNode = null
}
}
val nodeVal = (lastNode ?: getTimeoutActiveWindow()) ?: continue
val rightAppId = nodeVal.packageName?.toString() ?: break
val matchApp = rule.matchActivity(rightAppId)
if (topActivityFlow.value.appId != rightAppId || (!matchApp && rule is AppRule)) {
scope.launch(eventDispatcher) { fixAppId(rightAppId) }
return
}
if (!matchApp) continue
val target = a11yContext.queryRule(rule, nodeVal) ?: continue
if (activityRule !== activityRuleFlow.value) break
if (rule.checkDelay() && rule.actionDelayJob.value == null) {
rule.actionDelayJob.value = scope.launch(actionDispatcher) {
delay(rule.actionDelay)
rule.actionDelayJob.value = null
startQueryJob(byDelayRule = rule)
}
continue
}
if (rule.status != RuleStatus.StatusOk) break
val actionResult = rule.performAction(target)
if (actionResult.result) {
val topActivity = topActivityFlow.value
rule.trigger()
scope.launch(actionDispatcher) {
delay(300)
startQueryJob()
}
if (actionResult.action != ActionPerformer.None.action) {
showActionToast(rule)
}
addActionLog(rule, topActivity, target, actionResult)
}
}
}
companion object {
val service: A11yCommonImpl?
get() = uiAutomationFlow.value?.takeIf {
it.mode.value == latestServiceMode.value
} ?: A11yService.instance
val instance: A11yRuleEngine? get() = service?.ruleEngine
fun compatWindows(): List {
return try {
service?.windowInfos
} catch (_: Throwable) {
null
} ?: emptyList()
}
fun onScreenForcedActive() {
instance?.onScreenForcedActive()
}
fun performActionBack(): Boolean {
return (shizukuContextFlow.value.inputManager?.key(KeyEvent.KEYCODE_BACK)
?: A11yService.instance?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)) == true
}
suspend fun screenshot(): Bitmap? = service?.screenshot()
suspend fun execAction(gkdAction: GkdAction): ActionResult {
val selector = Selector.parseOrNull(gkdAction.selector) ?: throw RpcError("非法选择器")
runCatching { selector.checkType(typeInfo) }.exceptionOrNull()?.let {
throw RpcError("选择器类型错误:${it.message}")
}
val s = instance ?: throw RpcError("服务未连接")
val a = s.safeActiveWindow ?: throw RpcError("界面没有节点信息")
val targetNode = A11yContext(s, interruptable = false).querySelfOrSelector(
a, selector, MatchOption(fastQuery = gkdAction.fastQuery)
) ?: throw RpcError("没有查询到节点")
return withContext(Dispatchers.IO) {
ActionPerformer.getAction(gkdAction.action ?: ActionPerformer.None.action)
.perform(targetNode, gkdAction.position)
}
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/a11y/A11yState.kt
================================================
package li.songe.gkd.a11y
import android.content.ComponentName
import android.content.Intent
import android.provider.Settings
import android.util.LruCache
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import li.songe.gkd.META
import li.songe.gkd.app
import li.songe.gkd.appScope
import li.songe.gkd.data.ActionLog
import li.songe.gkd.data.ActionResult
import li.songe.gkd.data.ActivityLog
import li.songe.gkd.data.AttrInfo
import li.songe.gkd.data.ResetMatchType
import li.songe.gkd.data.ResolvedRule
import li.songe.gkd.data.RuleStatus
import li.songe.gkd.data.isSystem
import li.songe.gkd.db.DbSet
import li.songe.gkd.service.updateTopTaskAppId
import li.songe.gkd.shizuku.safeInvokeShizuku
import li.songe.gkd.store.actionCountFlow
import li.songe.gkd.store.checkAppBlockMatch
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.PKG_FLAGS
import li.songe.gkd.util.RuleSummary
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.ruleSummaryFlow
import li.songe.gkd.util.systemUiAppId
import li.songe.loc.Loc
data class TopActivity(
val appId: String = "",
val activityId: String? = null,
val number: Int = 0
) {
val shortActivityId: String?
get() {
val a = if (activityId != null && activityId.startsWith(appId)) {
activityId.substring(appId.length)
} else {
activityId
}
return a
}
fun format(): String {
return "${appId}/${shortActivityId}/${number}"
}
fun sameAs(a: String, b: String?): Boolean {
return appId == a && activityId == b
}
fun sameAs(cn: ComponentName): Boolean {
return appId == cn.packageName && activityId == cn.className
}
}
val topActivityFlow = MutableStateFlow(TopActivity())
private var lastValidActivity: TopActivity = topActivityFlow.value
set(value) {
if (value.activityId != null) {
field = value
}
}
private var activityLogCount = 0
private var lastActivityUpdateTime = 0L
private var lastActivityForceUpdateTime = 0L
private val tempActivityLogList = mutableListOf()
private object ActivityCache : LruCache, Boolean>(256) {
override fun create(key: Pair): Boolean = try {
app.packageManager.getActivityInfo(
ComponentName(key.first, key.second),
PKG_FLAGS
)
true
} catch (_: Exception) {
false
}
}
fun isActivity(
appId: String,
activityId: String,
): Boolean {
return topActivityFlow.value.sameAs(appId, activityId) || ActivityCache.get(appId to activityId)
}
class ActivityRule(
val topActivity: TopActivity = TopActivity(),
val ruleSummary: RuleSummary = RuleSummary(),
) {
val blockMatch = checkAppBlockMatch(topActivity.appId)
val appRules = ruleSummary.appIdToRules[topActivity.appId] ?: emptyList()
val activityRules = if (blockMatch) emptyList() else appRules.filter { rule ->
rule.matchActivity(topActivity.appId, topActivity.activityId)
}
val globalRules = if (blockMatch) emptyList() else ruleSummary.globalRules.filter { r ->
r.matchActivity(topActivity.appId, topActivity.activityId)
}
val currentRules = (activityRules + globalRules).sortedBy { it.order }
val hasPriorityRule = currentRules.size > 1 && currentRules.any { it.priorityEnabled }
val activePriority: Boolean
get() = hasPriorityRule && currentRules.any { it.isPriority() }
val priorityRules: List
get() = if (hasPriorityRule) {
currentRules.sortedBy { if (it.isPriority()) 0 else 1 }
} else {
currentRules
}
val skipMatch: Boolean
get() {
return currentRules.all { r -> !r.status.ok }
}
val skipConsumeEvent: Boolean
get() {
return currentRules.all { r -> !r.status.alive }
}
val hasFeatureAction: Boolean
get() = currentRules.any { r -> r.checkForced() && (r.status == RuleStatus.StatusOk || r.status == RuleStatus.Status5) }
}
val activityRuleFlow = MutableStateFlow(ActivityRule())
private var lastAppId = ""
sealed class ActivityScene {
data object ScreenOn : ActivityScene()
data object A11y : ActivityScene()
data object TaskStack : ActivityScene()
}
@Loc
@Synchronized
fun updateTopActivity(
appId: String,
activityId: String?,
scene: ActivityScene = ActivityScene.A11y,
@Loc loc: String = "",
) {
val t = System.currentTimeMillis()
if (scene == ActivityScene.TaskStack) {
updateTopTaskAppId(appId)
}
val oldActivity = topActivityFlow.value
val isSame = scene != ActivityScene.ScreenOn && oldActivity.sameAs(appId, activityId)
if (scene == ActivityScene.TaskStack) {
lastActivityForceUpdateTime = t
} else if (scene == ActivityScene.A11y) {
if (lastActivityForceUpdateTime > 0) {
// ITaskStackListener 的变速快于无障碍
if (t - lastActivityForceUpdateTime < 1000) return
if (activityId != null && t - lastActivityForceUpdateTime < 3000) return
}
if (isSame && t - lastActivityUpdateTime < 1000) return
}
val number = if (isSame) {
oldActivity.number + 1
} else {
0
}
topActivityFlow.value = TopActivity(
appId = appId,
activityId = activityId ?: lastValidActivity.takeIf { it.appId == appId }?.activityId,
number = number,
)
lastValidActivity = oldActivity
lastActivityUpdateTime = t
tempActivityLogList.add(
ActivityLog(
appId = appId,
activityId = activityId,
ctime = t,
)
)
if (tempActivityLogList.size >= 16 || appId == META.appId) {
val logs = tempActivityLogList.toTypedArray()
tempActivityLogList.clear()
appScope.launchTry {
DbSet.activityLogDao.insert(*logs)
}
}
if (activityLogCount++ % 100 == 0) {
appScope.launchTry { DbSet.activityLogDao.deleteKeepLatest() }
}
val topActivity = topActivityFlow.value
val oldActivityRule = activityRuleFlow.value
val ruleSummary = ruleSummaryFlow.value
val idChanged = (scene == ActivityScene.ScreenOn ||
topActivity.appId != oldActivityRule.topActivity.appId)
val topChanged = idChanged || oldActivityRule.topActivity != topActivity
val ruleChanged = oldActivityRule.ruleSummary !== ruleSummary
if (topChanged || ruleChanged) {
val newActivityRule = ActivityRule(
ruleSummary = ruleSummary,
topActivity = topActivity,
)
if (idChanged) {
val oldAppId = lastAppId
lastAppId = appId
appScope.launchTry {
DbSet.appVisitLogDao.insert(oldAppId, appId, t)
}
appChangeTime = t
ruleSummary.globalRules.forEach { it.resetState(t) }
ruleSummary.appIdToRules[oldActivityRule.topActivity.appId]?.forEach { it.resetState(t) }
newActivityRule.appRules.forEach { it.resetState(t) }
} else {
newActivityRule.currentRules.forEach { r ->
when (r.resetMatchType) {
ResetMatchType.App -> {
if (r.isFirstMatchApp) {
r.resetState(t)
}
}
ResetMatchType.Activity -> r.resetState(t)
ResetMatchType.Match -> {
// is new rule
if (!oldActivityRule.currentRules.contains(r)) {
r.resetState(t)
}
}
}
}
}
activityRuleFlow.value = newActivityRule
LogUtils.d(
"${oldActivity.format()} -> ${topActivityFlow.value.format()} (scene=$scene)",
loc = loc,
tag = "updateTopActivity",
)
}
}
@Volatile
var lastTriggerRule: ResolvedRule? = null
@Volatile
var lastTriggerTime = 0L
@Volatile
var appChangeTime = 0L
var imeAppId = ""
var launcherAppId = ""
var systemRecentCn = ComponentName("", "")
fun updateSystemDefaultAppId() {
imeAppId = app.getSecureString(Settings.Secure.DEFAULT_INPUT_METHOD)
?.let(ComponentName::unflattenFromString)?.packageName ?: ""
val launcherCn = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
.resolveActivity(app.packageManager)
launcherAppId = launcherCn.packageName
if (app.getPkgInfo(launcherAppId)?.applicationInfo?.isSystem == true) {
systemRecentCn = launcherCn
} else {
safeInvokeShizuku {
if (AndroidTarget.P) {
systemRecentCn = ComponentName.unflattenFromString(
app.getString(com.android.internal.R.string.config_recentsComponentName)
) ?: systemRecentCn
}
}
if (systemRecentCn.packageName.isEmpty()) {
// https://github.com/android-cs/8/blob/main/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
systemRecentCn = ComponentName(
systemUiAppId,
"$systemUiAppId.recents.RecentsActivity",
)
}
}
}
private val actionLogMutex = Mutex()
fun addActionLog(
rule: ResolvedRule,
topActivity: TopActivity,
target: AccessibilityNodeInfo,
actionResult: ActionResult,
) = appScope.launchTry(Dispatchers.IO) {
val ctime = System.currentTimeMillis()
actionLogMutex.withLock {
val actionLog = ActionLog(
appId = topActivity.appId,
activityId = topActivity.activityId,
subsId = rule.subsItem.id,
subsVersion = rule.rawSubs.version,
groupKey = rule.g.group.key,
groupType = rule.g.group.groupType,
ruleIndex = rule.index,
ruleKey = rule.key,
ctime = ctime,
)
DbSet.actionLogDao.insert(actionLog)
if (actionCountFlow.value % 100 == 0L) {
DbSet.actionLogDao.deleteKeepLatest()
}
}
LogUtils.d(
rule.statusText(),
AttrInfo.info2data(target, 0, 0),
actionResult
)
}.let {}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/A11yEventLog.kt
================================================
package li.songe.gkd.data
import android.view.accessibility.AccessibilityEvent
import androidx.paging.PagingSource
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import li.songe.gkd.a11y.STATE_CHANGED
@Serializable
@Entity(tableName = "a11y_event_log")
class A11yEventLog(
@PrimaryKey @ColumnInfo(name = "id") val id: Int,
@ColumnInfo(name = "ctime") val ctime: Long,
@ColumnInfo(name = "type") val type: Int,
@ColumnInfo(name = "appId") val appId: String,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "desc") val desc: String?,
@ColumnInfo(name = "text") val text: List,
) {
override fun equals(other: Any?): Boolean {
if (other !is A11yEventLog) return false
return id == other.id
}
override fun hashCode(): Int {
return id
}
val isStateChanged: Boolean
get() = type == STATE_CHANGED
val fixedName: String
get() {
if (isStateChanged && name.startsWith(appId)) {
return name.substring(appId.length)
}
if (name.contains("View") || name.contains("Layout") || viewSuffixes.any {
name.startsWith(
it
)
}) {
return name.substring(name.lastIndexOf('.') + 1)
}
return name
}
@Dao
interface A11yEventLogDao {
@Insert
suspend fun insert(objects: List): List
@Query("DELETE FROM a11y_event_log")
suspend fun deleteAll()
@Query("SELECT COUNT(*) FROM a11y_event_log")
fun count(): Flow
@Query("SELECT * FROM a11y_event_log ORDER BY ctime DESC ")
fun pagingSource(): PagingSource
@Query("SELECT MAX(id) FROM a11y_event_log")
suspend fun maxId(): Int?
@Query(
"""
DELETE FROM a11y_event_log
WHERE (
SELECT COUNT(*)
FROM a11y_event_log
) > 1000
AND id <= (
SELECT id
FROM a11y_event_log
ORDER BY id DESC
LIMIT 1 OFFSET 1000
)
"""
)
suspend fun deleteKeepLatest(): Int
}
}
private val viewSuffixes = listOf(
"android.widget.",
"android.view.",
"android.support.",
)
fun AccessibilityEvent.toA11yEventLog(id: Int) = A11yEventLog(
id = id,
ctime = System.currentTimeMillis(),
type = eventType,
appId = packageName.toString(),
name = className.toString(),
desc = contentDescription?.toString(),
text = text.map { it.toString() }
)
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/ActionLog.kt
================================================
package li.songe.gkd.data
import androidx.paging.PagingSource
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.DeleteTable
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.migration.AutoMigrationSpec
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import li.songe.gkd.util.format
import li.songe.gkd.util.getShowActivityId
@Serializable
@Entity(
tableName = "action_log",
)
data class ActionLog(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0,
@ColumnInfo(name = "ctime") val ctime: Long,
@ColumnInfo(name = "app_id") val appId: String,
@ColumnInfo(name = "activity_id") val activityId: String? = null,
@ColumnInfo(name = "subs_id") val subsId: Long,
@ColumnInfo(name = "subs_version", defaultValue = "0") val subsVersion: Int,
@ColumnInfo(name = "group_key") val groupKey: Int,
@ColumnInfo(name = "group_type", defaultValue = "2") val groupType: Int,
@ColumnInfo(name = "rule_index") val ruleIndex: Int,
@ColumnInfo(name = "rule_key") val ruleKey: Int? = null,
) {
val showActivityId by lazy { getShowActivityId(appId, activityId) }
val date by lazy { ctime.format("MM-dd HH:mm:ss SSS") }
@DeleteTable.Entries(
DeleteTable(tableName = "click_log")
)
class ActionLogSpec : AutoMigrationSpec
@Dao
interface ActionLogDao {
@Insert
suspend fun insert(vararg objects: ActionLog): List
@Query("DELETE FROM action_log WHERE subs_id IN (:subsIds)")
suspend fun deleteBySubsId(vararg subsIds: Long): Int
@Query("DELETE FROM action_log")
suspend fun deleteAll()
@Query("DELETE FROM action_log WHERE subs_id=:subsId")
suspend fun deleteSubsAll(subsId: Long)
@Query("DELETE FROM action_log WHERE app_id=:appId")
suspend fun deleteAppAll(appId: String)
@Query("SELECT * FROM action_log ORDER BY id DESC LIMIT 1000")
fun query(): Flow>
@Query("SELECT * FROM action_log ORDER BY id DESC ")
fun pagingSource(): PagingSource
@Query("SELECT * FROM action_log WHERE subs_id=:subsId ORDER BY id DESC ")
fun pagingSubsSource(subsId: Long): PagingSource
@Query("SELECT * FROM action_log WHERE app_id=:appId ORDER BY id DESC ")
fun pagingAppSource(appId: String): PagingSource
@Query("SELECT COUNT(*) FROM action_log")
fun count(): Flow
@Query("SELECT * FROM action_log ORDER BY id DESC LIMIT 1")
fun queryLatest(): Flow
@Query(
"""
SELECT cl.* FROM action_log AS cl
INNER JOIN (
SELECT subs_id, group_type, group_key, MAX(id) AS max_id FROM action_log
WHERE app_id = :appId AND subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)
GROUP BY subs_id, group_type, group_key
) AS latest_log ON cl.subs_id = latest_log.subs_id
AND cl.group_type = latest_log.group_type
AND cl.group_key = latest_log.group_key
AND cl.id = latest_log.max_id
"""
)
fun queryLatestByAppId(appId: String): Flow>
@Query(
"""
DELETE FROM action_log
WHERE (
SELECT COUNT(*)
FROM action_log
) > 500
AND id <= (
SELECT id
FROM action_log
ORDER BY id DESC
LIMIT 1 OFFSET 500
)
"""
)
suspend fun deleteKeepLatest(): Int
@Query("SELECT DISTINCT app_id FROM action_log ORDER BY id DESC")
fun queryLatestUniqueAppIds(): Flow>
@Query("SELECT DISTINCT app_id FROM action_log WHERE subs_id=:subsItemId AND group_type=${SubsConfig.AppGroupType} ORDER BY id DESC")
fun queryLatestUniqueAppIds(subsItemId: Long): Flow>
@Query("SELECT DISTINCT app_id FROM action_log WHERE subs_id=:subsItemId AND group_key=:globalGroupKey AND group_type=${SubsConfig.GlobalGroupType} ORDER BY id DESC")
fun queryLatestUniqueAppIds(subsItemId: Long, globalGroupKey: Int): Flow>
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/ActivityLog.kt
================================================
package li.songe.gkd.data
import androidx.paging.PagingSource
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.DeleteTable
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.migration.AutoMigrationSpec
import kotlinx.coroutines.flow.Flow
import li.songe.gkd.util.format
import li.songe.gkd.util.getShowActivityId
@Entity(
tableName = "activity_log_v2",
)
data class ActivityLog(
// 不使用时间戳作为主键的原因
// https://github.com/gkd-kit/gkd/issues/704
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0,
@ColumnInfo(name = "ctime") val ctime: Long,
@ColumnInfo(name = "app_id") val appId: String,
@ColumnInfo(name = "activity_id") val activityId: String? = null,
) {
val showActivityId by lazy { getShowActivityId(appId, activityId) }
val date by lazy { ctime.format("HH:mm:ss SSS") }
@Dao
interface ActivityLogDao {
@Insert
suspend fun insert(vararg objects: ActivityLog): List
@Query("DELETE FROM activity_log_v2")
suspend fun deleteAll()
@Query("SELECT * FROM activity_log_v2 ORDER BY ctime DESC ")
fun pagingSource(): PagingSource
@Query("SELECT COUNT(*) FROM activity_log_v2")
fun count(): Flow
@Query(
"""
DELETE FROM activity_log_v2
WHERE (
SELECT COUNT(*)
FROM activity_log_v2
) > 500
AND ctime <= (
SELECT ctime
FROM activity_log_v2
ORDER BY ctime DESC
LIMIT 1 OFFSET 500
)
"""
)
suspend fun deleteKeepLatest(): Int
}
@DeleteTable.Entries(
DeleteTable(tableName = "activity_log")
)
class ActivityLogV2Spec : AutoMigrationSpec
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/AppConfig.kt
================================================
package li.songe.gkd.data
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
@Serializable
@Entity(
tableName = "app_config",
)
data class AppConfig(
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
@ColumnInfo(name = "enable") val enable: Boolean,
@ColumnInfo(name = "subs_id") val subsId: Long,
@ColumnInfo(name = "app_id") val appId: String,
) {
@Dao
interface AppConfigDao {
@Update
suspend fun update(vararg objects: AppConfig): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg users: AppConfig): List
@Query("SELECT * FROM app_config WHERE subs_id=:subsId")
fun queryAppTypeConfig(subsId: Long): Flow>
@Query("SELECT * FROM app_config WHERE app_id=:appId AND subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)")
fun queryAppUsedList(appId: String): Flow>
@Query("SELECT * FROM app_config WHERE subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)")
fun queryUsedList(): Flow>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(vararg objects: AppConfig): List
@Query("SELECT * FROM app_config WHERE subs_id IN (:subsItemIds)")
suspend fun querySubsItemConfig(subsItemIds: List): List
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/AppInfo.kt
================================================
package li.songe.gkd.data
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import kotlinx.serialization.Serializable
import li.songe.gkd.app
import li.songe.gkd.shizuku.casted
import li.songe.gkd.shizuku.currentUserId
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.pkgIcon
@Serializable
data class AppInfo(
val id: String,
val name: String,
val versionCode: Int,
val versionName: String?,
val isSystem: Boolean,
val mtime: Long,
val hidden: Boolean,
val enabled: Boolean,
val userId: Int,
) {
override fun equals(other: Any?): Boolean {
if (other !is AppInfo) return false
return id == other.id && mtime == other.mtime && userId == other.userId
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + mtime.hashCode()
result = 31 * result + userId
return result
}
}
val selfAppInfo by lazy {
app.packageManager.getPackageInfo(app.packageName, 0).toAppInfo()
}
private val PackageInfo.compatVersionCode: Int
get() = if (AndroidTarget.P) {
longVersionCode.toInt()
} else {
@Suppress("DEPRECATION")
versionCode
}
val ApplicationInfo.isSystem: Boolean
get() = flags and ApplicationInfo.FLAG_SYSTEM != 0 || flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0
private fun checkHasActivity(packageName: String): Boolean {
return app.packageManager.getLaunchIntentForPackage(packageName) != null || app.packageManager.queryIntentActivities(
Intent().setPackage(packageName),
PackageManager.MATCH_DISABLED_COMPONENTS
).isNotEmpty() || try {
app.packageManager.getPackageInfo(
packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES
).activities?.isNotEmpty() == true
} catch (_: Throwable) {
// #1195 packageManager.getPackageInfo android.os.DeadSystemRuntimeException
true
}
}
private fun PackageInfo.getEnabled(userId: Int): Boolean {
val enabled = applicationInfo?.enabled ?: true
if (enabled) return true
val state = try {
// https://github.com/gkd-kit/gkd/issues/1169#issuecomment-3489260246
if (userId == currentUserId) {
app.packageManager.getApplicationEnabledSetting(packageName)
} else {
shizukuContextFlow.value.packageManager?.getApplicationEnabledSetting(
packageName,
currentUserId
)
}
} catch (_: IllegalArgumentException) {
null
}
return when (state) {
null,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> false
else -> true
}
}
// all->433 isOverlay->354 checkAppHasActivity->271
fun PackageInfo.toAppInfo(
userId: Int = currentUserId,
hidden: Boolean? = null,
): AppInfo {
val isSystem = applicationInfo?.isSystem ?: false
return AppInfo(
userId = userId,
id = packageName,
versionCode = compatVersionCode,
versionName = versionName,
mtime = lastUpdateTime,
isSystem = isSystem,
name = applicationInfo?.run { loadLabel(app.packageManager).toString() } ?: packageName,
hidden = hidden ?: (isSystem && (casted.overlayTarget != null || !checkHasActivity(
packageName
))),
enabled = getEnabled(userId),
)
}
fun PackageInfo.toAppInfoAndIcon(
userId: Int = currentUserId,
hidden: Boolean? = null,
): Pair {
val appInfo = toAppInfo(userId, hidden)
return if (appInfo.hidden) {
appInfo to null
} else {
appInfo to pkgIcon
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/AppRule.kt
================================================
package li.songe.gkd.data
class AppRule(
rule: RawSubscription.RawAppRule,
g: ResolvedAppGroup,
appInfo: AppInfo?,
) : ResolvedRule(
rule = rule,
g = g,
) {
val group = g.group
val app = g.app
val enable = appInfo?.let {
if (rule.versionCode?.match(it.versionCode) == false) {
return@let false
}
if (rule.versionName?.match(it.versionName) == false) {
return@let false
}
null
} ?: true
val appId = app.id
private val activityIds = getFixActivityIds(app.id, rule.activityIds ?: group.activityIds)
private val excludeActivityIds =
(getFixActivityIds(
app.id,
rule.excludeActivityIds ?: group.excludeActivityIds
) + (excludeData.activityIds.filter { e -> e.first == appId }
.map { e -> e.second })).distinct()
override val type = "app"
override fun matchActivity(appId: String, activityId: String?): Boolean {
if (!enable) return false
if (appId != app.id) return false
activityId ?: return true
if (excludeActivityIds.any { activityId.startsWith(it) }) return false
return activityIds.isEmpty() || activityIds.any { activityId.startsWith(it) }
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/AppVisitLog.kt
================================================
package li.songe.gkd.data
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
import li.songe.gkd.META
import li.songe.gkd.a11y.launcherAppId
import li.songe.gkd.util.systemUiAppId
@Entity(
tableName = "app_visit_log",
)
data class AppVisitLog(
@PrimaryKey() @ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "mtime") val mtime: Long,
) {
@Dao
interface AppLogDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg objects: AppVisitLog): List
@Transaction
suspend fun insert(oldAppId: String, newAppId: String, mtime: Long) {
insert(
AppVisitLog(oldAppId, fixAppVisitTime(oldAppId, mtime - 1)),
AppVisitLog(newAppId, fixAppVisitTime(newAppId, mtime)),
)
if (appLogCount++ % 100 == 0) {
deleteKeepLatest()
}
}
@Query("SELECT DISTINCT id FROM app_visit_log ORDER BY mtime DESC")
fun query(): Flow>
@Query(
"""
DELETE FROM app_visit_log
WHERE (
SELECT COUNT(*)
FROM app_visit_log
) > 500
AND mtime <= (
SELECT mtime
FROM app_visit_log
ORDER BY mtime DESC
LIMIT 1 OFFSET 500
)
"""
)
suspend fun deleteKeepLatest(): Int
}
}
private fun fixAppVisitTime(appId: String, t: Long): Long = when (appId) {
META.appId, launcherAppId, systemUiAppId -> t - 60_000
else -> t
}
private var appLogCount = 0
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/AttrInfo.kt
================================================
package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
import li.songe.gkd.a11y.compatChecked
import li.songe.gkd.shizuku.casted
@Serializable
data class AttrInfo(
val id: String?,
val vid: String?,
val name: String?,
val text: String?,
val desc: String?,
val clickable: Boolean,
val focusable: Boolean,
val checkable: Boolean,
val checked: Boolean?,
val editable: Boolean,
val longClickable: Boolean,
val visibleToUser: Boolean,
val left: Int,
val top: Int,
val right: Int,
val bottom: Int,
val width: Int,
val height: Int,
val childCount: Int,
val index: Int,
val depth: Int,
) {
companion object {
fun info2data(
node: AccessibilityNodeInfo,
index: Int,
depth: Int,
): AttrInfo {
val rect = node.casted.boundsInScreen
val appId = node.packageName?.toString() ?: ""
val id: String? = node.viewIdResourceName
val idPrefix = "$appId:id/"
val vid = if (id != null && id.startsWith(idPrefix)) {
id.substring(idPrefix.length)
} else {
// 此处不使用 id 是因为某些节点的 id 没有 appId:id/ 前缀
null
}
return AttrInfo(
id = id,
vid = vid,
name = node.className?.toString(),
text = node.text?.toString(),
desc = node.contentDescription?.toString(),
clickable = node.isClickable,
focusable = node.isFocusable,
checkable = node.isCheckable,
checked = node.compatChecked,
editable = node.isEditable,
longClickable = node.isLongClickable,
visibleToUser = node.isVisibleToUser,
left = rect.left,
top = rect.top,
right = rect.right,
bottom = rect.bottom,
width = rect.width(),
height = rect.height(),
childCount = node.childCount,
index = index,
depth = depth,
)
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/BaseSnapshot.kt
================================================
package li.songe.gkd.data
interface BaseSnapshot {
val id: Long
val appId: String
val activityId: String?
val screenHeight: Int
val screenWidth: Int
val isLandscape: Boolean
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/CategoryConfig.kt
================================================
package li.songe.gkd.data
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
@Serializable
@Entity(
tableName = "category_config",
)
data class CategoryConfig(
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
@ColumnInfo(name = "enable") val enable: Boolean? = null,
@ColumnInfo(name = "subs_id") val subsId: Long,
@ColumnInfo(name = "category_key") val categoryKey: Int,
) {
@Dao
interface CategoryConfigDao {
@Update
suspend fun update(vararg objects: CategoryConfig): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg objects: CategoryConfig): List
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(vararg objects: CategoryConfig): List
@Delete
suspend fun delete(vararg objects: CategoryConfig): Int
@Query("DELETE FROM category_config WHERE subs_id=:subsItemId")
suspend fun deleteBySubsItemId(subsItemId: Long): Int
@Query("DELETE FROM category_config WHERE subs_id IN (:subsIds)")
suspend fun deleteBySubsId(vararg subsIds: Long): Int
@Query("DELETE FROM category_config WHERE subs_id=:subsItemId AND category_key=:categoryKey")
suspend fun deleteByCategoryKey(subsItemId: Long, categoryKey: Int): Int
@Query("SELECT * FROM category_config WHERE subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)")
fun queryUsedList(): Flow>
@Query("SELECT * FROM category_config WHERE subs_id=:subsItemId")
fun queryConfig(subsItemId: Long): Flow>
@Query("SELECT * FROM category_config WHERE subs_id=:subsId AND category_key=:categoryKey")
suspend fun queryCategoryConfig(subsId: Long, categoryKey: Int): CategoryConfig?
@Query("SELECT * FROM category_config WHERE subs_id IN (:subsItemIds)")
suspend fun querySubsItemConfig(subsItemIds: List): List
@Query("SELECT * FROM category_config WHERE subs_id IN (:subsItemIds)")
fun queryBySubsIds(subsItemIds: List): Flow>
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt
================================================
package li.songe.gkd.data
import kotlinx.serialization.Serializable
import li.songe.gkd.util.appInfoMapFlow
@Serializable
data class ComplexSnapshot(
override val id: Long,
override val appId: String,
override val activityId: String?,
override val screenHeight: Int,
override val screenWidth: Int,
override val isLandscape: Boolean,
val appInfo: AppInfo? = appInfoMapFlow.value[appId],
val gkdAppInfo: AppInfo? = selfAppInfo,
val device: DeviceInfo = DeviceInfo(),
val nodes: List,
) : BaseSnapshot {
fun toSnapshot(): Snapshot {
return Snapshot(
id = id,
appId = appId,
activityId = activityId,
screenHeight = screenHeight,
screenWidth = screenWidth,
isLandscape = isLandscape,
)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/DeviceInfo.kt
================================================
package li.songe.gkd.data
import android.os.Build
import kotlinx.serialization.Serializable
@Serializable
data class DeviceInfo(
val device: String = Build.DEVICE,
val model: String = Build.MODEL,
val manufacturer: String = Build.MANUFACTURER,
val brand: String = Build.BRAND,
val sdkInt: Int = Build.VERSION.SDK_INT,
val release: String = Build.VERSION.RELEASE,
)
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/GithubPoliciesAsset.kt
================================================
package li.songe.gkd.data
import kotlinx.serialization.Serializable
import li.songe.gkd.util.FILE_SHORT_URL
@Serializable
data class GithubPoliciesAsset(
val id: Int,
val href: String,
) {
val shortHref: String
get() = FILE_SHORT_URL + id
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/GkdAction.kt
================================================
package li.songe.gkd.data
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
import li.songe.gkd.a11y.A11yRuleEngine
import li.songe.gkd.service.A11yService
import li.songe.gkd.shizuku.casted
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.util.ScreenUtils
@Serializable
data class GkdAction(
val selector: String,
val fastQuery: Boolean = false,
val action: String? = null,
val position: RawSubscription.Position? = null,
)
@Serializable
data class ActionResult(
val action: String,
val result: Boolean,
val shizuku: Boolean = false,
val position: Pair? = null,
)
sealed class ActionPerformer(val action: String) {
abstract fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult
data object ClickNode : ActionPerformer("clickNode") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
return ActionResult(
action = action,
result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
)
}
}
data object ClickCenter : ActionPerformer("clickCenter") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
val rect = node.casted.boundsInScreen
val p = position?.calc(rect)
val x = p?.first ?: ((rect.right + rect.left) / 2f)
val y = p?.second ?: ((rect.bottom + rect.top) / 2f)
return ActionResult(
action = action,
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
if (shizukuContextFlow.value.tap(x, y)) {
return ActionResult(
action = action, result = true, shizuku = true, position = x to y
)
}
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
gestureDescription.addStroke(
GestureDescription.StrokeDescription(
path, 0, ViewConfiguration.getTapTimeout().toLong()
)
)
A11yService.instance?.dispatchGesture(
gestureDescription.build(), null, null
) != null
} else {
false
},
position = x to y
)
}
}
data object Click : ActionPerformer("click") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
if (node.isClickable) {
val result = ClickNode.perform(node, position)
if (result.result) {
return result
}
}
return ClickCenter.perform(node, position)
}
}
data object LongClickNode : ActionPerformer("longClickNode") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
return ActionResult(
action = action,
result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
)
}
}
data object LongClickCenter : ActionPerformer("longClickCenter") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
val rect = node.casted.boundsInScreen
val p = position?.calc(rect)
val x = p?.first ?: ((rect.right + rect.left) / 2f)
val y = p?.second ?: ((rect.bottom + rect.top) / 2f)
// 某些系统的 ViewConfiguration.getLongPressTimeout() 返回 300 , 这将导致触发普通的 click 事件
val longClickDuration = 500L
return ActionResult(
action = action,
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
if (shizukuContextFlow.value.tap(
x, y, longClickDuration
)
) {
return ActionResult(
action = action, result = true, shizuku = true, position = x to y
)
}
val gestureDescription = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
gestureDescription.addStroke(
GestureDescription.StrokeDescription(
path, 0, longClickDuration
)
)
A11yService.instance?.dispatchGesture(
gestureDescription.build(), null, null
) != null
} else {
false
},
position = x to y
)
}
}
data object LongClick : ActionPerformer("longClick") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
if (node.isLongClickable) {
val result = LongClickNode.perform(node, position)
if (result.result) {
return result
}
}
return LongClickCenter.perform(node, position)
}
}
data object Back : ActionPerformer("back") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
return ActionResult(
action = action,
result = A11yRuleEngine.performActionBack()
)
}
}
data object None : ActionPerformer("none") {
override fun perform(
node: AccessibilityNodeInfo,
position: RawSubscription.Position?,
): ActionResult {
return ActionResult(
action = action, result = true
)
}
}
companion object {
private val allSubObjects by lazy {
arrayOf(
ClickNode, ClickCenter, Click, LongClickNode, LongClickCenter, LongClick, Back, None
)
}
fun getAction(action: String?): ActionPerformer {
return allSubObjects.find { it.action == action } ?: Click
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt
================================================
package li.songe.gkd.data
import li.songe.gkd.a11y.launcherAppId
import li.songe.gkd.util.systemAppsFlow
data class GlobalApp(
val id: String,
val enable: Boolean,
val activityIds: List,
val excludeActivityIds: List,
)
class GlobalRule(
rule: RawSubscription.RawGlobalRule,
g: ResolvedGlobalGroup,
appInfoCache: Map,
) : ResolvedRule(
rule = rule,
g = g,
) {
val groupExcludeAppIds = g.groupExcludeAppIds
val group = g.group
private val matchAnyApp = rule.matchAnyApp ?: group.matchAnyApp ?: true
private val matchLauncher = rule.matchLauncher ?: group.matchLauncher ?: false
private val matchSystemApp = rule.matchSystemApp ?: group.matchSystemApp ?: false
val apps = mutableMapOf().apply {
(rule.apps ?: group.apps ?: emptyList()).filter { a ->
// https://github.com/gkd-kit/gkd/issues/619
appInfoCache.isEmpty() || appInfoCache.containsKey(a.id) // 过滤掉未安装应用
}.forEach { a ->
val enable = a.enable ?: appInfoCache[a.id]?.let { appInfo ->
if (a.versionCode?.match(appInfo.versionCode) == false) {
return@let false
}
if (a.versionName?.match(appInfo.versionName) == false) {
return@let false
}
null
} ?: true
this[a.id] = GlobalApp(
id = a.id,
enable = enable,
activityIds = getFixActivityIds(a.id, a.activityIds),
excludeActivityIds = getFixActivityIds(a.id, a.excludeActivityIds),
)
}
}
override val type = "global"
private val excludeAppIds = apps.filter { e ->
!e.value.enable
}.keys
private val enableApps = apps.filter { e -> e.value.enable }
/**
* 内置禁用>用户配置>规则自带
* 范围越精确优先级越高
*/
override fun matchActivity(appId: String, activityId: String?): Boolean {
// 规则自带禁用
if (excludeAppIds.contains(appId) || groupExcludeAppIds.contains(appId)) {
return false
}
// 用户自定义禁用
if (excludeData.excludeAppIds.contains(appId)) {
return false
}
if (activityId != null && excludeData.activityIds.contains(appId to activityId)) {
return false
}
if (excludeData.includeAppIds.contains(appId)) {
activityId ?: return true
val app = enableApps[appId] ?: return true
// 规则自带页面的禁用
return !app.excludeActivityIds.any { e -> e.startsWith(activityId) }
}
// 范围比较
val app = enableApps[appId]
if (app != null) { // 规则自定义启用
activityId ?: return true
return app.activityIds.isEmpty() || app.activityIds.any { e -> e.startsWith(activityId) }
} else {
if (!matchLauncher && appId == launcherAppId) {
return false
}
if (!matchSystemApp && systemAppsFlow.value.contains(appId)) {
return false
}
return matchAnyApp
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/NodeInfo.kt
================================================
package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.serialization.Serializable
import li.songe.gkd.a11y.MAX_CHILD_SIZE
import li.songe.gkd.a11y.topActivityFlow
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.toast
import kotlin.system.measureTimeMillis
@Serializable
data class NodeInfo(
val id: Int,
val pid: Int,
val idQf: Boolean?,
val textQf: Boolean?,
val attr: AttrInfo,
)
private data class TempNodeData(
val node: AccessibilityNodeInfo,
val parent: TempNodeData?,
val index: Int,
val depth: Int,
) {
var id = 0
val attr = AttrInfo.info2data(node, index, depth)
var children: List = emptyList()
var idQfInit = false
var idQf: Boolean? = null
set(value) {
field = value
idQfInit = true
}
var textQfInit = false
var textQf: Boolean? = null
set(value) {
field = value
textQfInit = true
}
}
private fun getChildren(node: AccessibilityNodeInfo) = sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i ->
val child = node.getChild(i) ?: return@sequence
yield(child)
}
}
private const val MAX_KEEP_SIZE = 5000
// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价
fun info2nodeList(root: AccessibilityNodeInfo?): List {
if (root == null) {
return emptyList()
}
val nodes = mutableListOf()
val collectTime = measureTimeMillis {
val stack = mutableListOf()
var times = 0
stack.add(TempNodeData(root, null, 0, 0))
while (stack.isNotEmpty()) {
times++
val node = stack.removeAt(stack.lastIndex)
node.id = times - 1
val children = getChildren(node.node).mapIndexed { i, child ->
TempNodeData(
child, node, i, node.depth + 1
)
}.toList()
node.children = children
nodes.add(node)
repeat(children.size) { i ->
stack.add(children[children.size - i - 1])
}
if (times > MAX_KEEP_SIZE) {
// https://github.com/gkd-kit/gkd/issues/28
toast("节点数量至多保留$MAX_KEEP_SIZE,丢弃后续节点")
LogUtils.d(
"节点数量过多",
root.packageName,
topActivityFlow.value.activityId,
)
break
}
}
}
val qfTime = measureTimeMillis {
val idQfCache = mutableMapOf>()
val textQfCache = mutableMapOf>()
var idTextQf = false
fun updateQf(n: TempNodeData) {
if (!n.idQfInit && !n.attr.id.isNullOrEmpty()) {
n.idQf = (idQfCache[n.attr.id]
?: root.findAccessibilityNodeInfosByViewId(n.attr.id)).apply {
idQfCache[n.attr.id] = this
}
.any { t -> t == n.node }
}
if (!n.textQfInit && !n.attr.text.isNullOrEmpty()) {
n.textQf = (textQfCache[n.attr.text]
?: root.findAccessibilityNodeInfosByText(n.attr.text)).apply {
textQfCache[n.attr.text] = this
}
.any { t -> t == n.node }
}
if (n.idQf == true && n.textQf == true) {
idTextQf = true
}
if (!n.idQfInit && n.idQf != null) {
n.parent?.children?.forEach { c ->
c.idQf = n.idQf
if (idTextQf) {
c.textQf = n.textQf
}
}
if (n.idQf == true) {
var p = n.parent
while (p != null && !p.idQfInit) {
p.idQf = n.idQf
if (idTextQf) {
p.textQf = n.textQf
}
p = p.parent
p?.children?.forEach { bro ->
bro.idQf = n.idQf
if (idTextQf) {
bro.textQf = n.textQf
}
}
}
} else {
val tempStack = mutableListOf(n)
while (tempStack.isNotEmpty()) {
val top = tempStack.removeAt(tempStack.lastIndex)
top.idQf = n.idQf
if (idTextQf) {
top.textQf = n.textQf
}
repeat(top.children.size) { i ->
tempStack.add(top.children[top.children.size - i - 1])
}
}
}
}
if (!n.textQfInit && n.textQf != null) {
n.parent?.children?.forEach { c ->
c.textQf = n.textQf
if (idTextQf) {
c.idQf = n.idQf
}
}
if (n.textQf == true) {
var p = n.parent
while (p != null && !p.textQfInit) {
p.textQf = n.textQf
if (idTextQf) {
p.idQf = n.idQf
}
p = p.parent
p?.children?.forEach { bro ->
bro.textQf = n.textQf
if (idTextQf) {
bro.idQf = bro.idQf
}
}
}
} else {
val tempStack = mutableListOf(n)
while (tempStack.isNotEmpty()) {
val top = tempStack.removeAt(tempStack.lastIndex)
top.textQf = n.textQf
if (idTextQf) {
top.idQf = n.idQf
}
repeat(top.children.size) { i ->
tempStack.add(top.children[top.children.size - i - 1])
}
}
}
}
n.idQfInit = true
n.textQfInit = true
}
for (i in (nodes.size - 1) downTo 0) {
val n = nodes[i]
if (n.children.isEmpty()) {
updateQf(n)
}
}
for (i in (nodes.size - 1) downTo 0) {
val n = nodes[i]
if (n.children.isNotEmpty()) {
updateQf(n)
}
}
}
LogUtils.d(
topActivityFlow.value,
"快照节点数量:${nodes.size}, 总耗时:${collectTime + qfTime}ms",
"收集节点耗时:${collectTime}ms, 收集 fastQuery 耗时:${qfTime}ms",
)
return nodes.map { n ->
NodeInfo(
id = n.id,
pid = n.parent?.id ?: -1,
idQf = n.idQf,
textQf = n.textQf,
attr = n.attr
)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt
================================================
package li.songe.gkd.data
import android.graphics.Rect
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import li.songe.gkd.a11y.typeInfo
import li.songe.gkd.util.LOCAL_SUBS_IDS
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.appInfoMapFlow
import li.songe.gkd.util.distinctByIfAny
import li.songe.gkd.util.filterIfNotAll
import li.songe.gkd.util.json
import li.songe.gkd.util.toJson5String
import li.songe.gkd.util.toast
import li.songe.json5.Json5
import li.songe.selector.Selector
import net.objecthunter.exp4j.Expression
import net.objecthunter.exp4j.ExpressionBuilder
import java.util.Objects
@Serializable
data class RawSubscription(
val id: Long,
val name: String,
val version: Int,
val author: String? = null,
val updateUrl: String? = null,
val supportUri: String? = null,
val checkUpdateUrl: String? = null,
val globalGroups: List = emptyList(),
val categories: List = emptyList(),
val apps: List = emptyList(),
) {
// 重写 equals 和 hashCode 便于 compose 重组比较
override fun equals(other: Any?): Boolean {
return other === this
}
override fun hashCode(): Int {
return Objects.hash(id, name, version)
}
val isEmpty: Boolean
get() = globalGroups.isEmpty() && apps.all { it.groups.isEmpty() } && categories.isEmpty()
val isLocal: Boolean
get() = LOCAL_SUBS_IDS.contains(id)
val hasRule get() = globalGroups.isNotEmpty() || apps.any { it.groups.isNotEmpty() }
val usedApps by lazy {
apps.run {
if (any { it.groups.isEmpty() }) {
filterNot { it.groups.isEmpty() }
} else {
this
}
}
}
fun getSafeCategory(key: Int): RawCategory {
return categories.find { it.key == key } ?: RawCategory(
key = key,
name = key.toString(),
enable = false,
desc = null
)
}
val categoryToGroupsMap by lazy {
val allAppGroups = apps.flatMap { a -> a.groups.map { g -> g to a } }
allAppGroups.groupBy { g ->
categories.find { c -> g.first.name.startsWith(c.name) }
}
}
private val categoryToAppMap by lazy {
val map = mutableMapOf>()
categories.forEach { c ->
apps.forEach { a ->
if (a.groups.any { g -> g.name.startsWith(c.name) }) {
val list = map[c.key]
if (list == null) {
map[c.key] = mutableListOf(a)
} else {
list.add(a)
}
}
}
}
map
}
fun getCategoryApps(categoryKey: Int): List {
return categoryToAppMap[categoryKey] ?: emptyList()
}
fun getAppGroups(appId: String): List {
return apps.find { a -> a.id == appId }?.groups ?: emptyList()
}
fun getApp(appId: String): RawApp {
return apps.find { a -> a.id == appId } ?: RawApp(
id = appId,
name = appInfoMapFlow.value[appId]?.name,
groups = emptyList()
)
}
val groupToCategoryMap by lazy {
val map = mutableMapOf()
categoryToGroupsMap.forEach { (key, value) ->
value.forEach { (g) ->
if (key != null) {
map[g] = key
}
}
}
map
}
val appGroups by lazy {
apps.flatMap { a -> a.groups }
}
val groupsSize by lazy {
appGroups.size + globalGroups.size
}
val globalGroupAppGroupNameDisableMap by lazy {
globalGroups.mapNotNull { g ->
val n = g.disableIfAppGroupMatch
if (n != null) {
val gName = n.ifEmpty { g.name }
g.key to apps.filter { a ->
a.groups.any { ag ->
ag.ignoreGlobalGroupMatch != true && ag.name.startsWith(gName)
}
}.map { it.id }.toHashSet()
} else {
null
}
}.toMap()
}
fun getGlobalGroupInnerDisabled(globalGroup: RawGlobalGroup, appId: String): Boolean {
globalGroup.appIdEnable[appId]?.let {
if (!it) return true
}
globalGroupAppGroupNameDisableMap[globalGroup.key]?.let {
if (it.contains(appId)) {
return true
}
}
return false
}
val numText by lazy {
val appsSize = apps.size
val appGroupsSize = appGroups.size
val globalGroupSize = globalGroups.size
if (appGroupsSize + globalGroupSize > 0) {
if (globalGroupSize > 0) {
"${globalGroupSize}全局" + if (appGroupsSize > 0) {
"/"
} else {
""
}
} else {
""
} + if (appGroupsSize > 0) {
"${appsSize}应用/${appGroupsSize}规则组"
} else {
""
}
} else {
"暂无规则"
}
}
@Serializable
data class StringMatcher(
val pattern: String?,
val include: List?,
val exclude: List?,
) {
private val patternRegex by lazy {
pattern?.let { p ->
runCatching { Regex(p) }.getOrNull()
}
}
fun match(value: String?): Boolean {
if (value == null) return false
if (exclude?.contains(value) == true) return false
if (include?.contains(value) == false) return false
if (patternRegex?.matches(value) == false) return false
return true
}
}
@Serializable
data class IntegerMatcher(
val minimum: Int?,
val maximum: Int?,
val include: List?,
val exclude: List?,
) {
fun match(value: Int?): Boolean {
if (value == null) return false
if (exclude?.contains(value) == true) return false
if (include?.contains(value) == false) return false
if (minimum != null && value < minimum) return false
if (maximum != null && value > maximum) return false
return true
}
}
@Serializable
data class RawApp(
val id: String,
val name: String?,
val groups: List = emptyList(),
)
@Serializable
data class RawCategory(
val key: Int,
val name: String,
val enable: Boolean?,
val desc: String?,
)
@Serializable
data class Position(
val left: String?, val top: String?, val right: String?, val bottom: String?
) {
private val leftExp by lazy { getExpression(left) }
private val topExp by lazy { getExpression(top) }
private val rightExp by lazy { getExpression(right) }
private val bottomExp by lazy { getExpression(bottom) }
val isValid by lazy {
((leftExp != null && (topExp != null || bottomExp != null)) || (rightExp != null && (topExp != null || bottomExp != null)))
}
/**
* return (x, y)
*/
fun calc(rect: Rect): Pair? {
if (!isValid) return null
arrayOf(
leftExp, topExp, rightExp, bottomExp
).forEach { exp ->
if (exp != null) {
setVariables(exp, rect)
}
}
try {
if (leftExp != null) {
if (topExp != null) {
return (rect.left + leftExp!!.evaluate()
.toFloat()) to (rect.top + topExp!!.evaluate().toFloat())
}
if (bottomExp != null) {
return (rect.left + leftExp!!.evaluate()
.toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())
}
} else if (rightExp != null) {
if (topExp != null) {
return (rect.right - rightExp!!.evaluate()
.toFloat()) to (rect.top + topExp!!.evaluate().toFloat())
}
if (bottomExp != null) {
return (rect.right - rightExp!!.evaluate()
.toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())
}
}
} catch (e: Exception) {
// 可能存在 1/0 导致错误
e.printStackTrace()
LogUtils.d(e)
toast(e.message ?: e.stackTraceToString())
}
return null
}
}
sealed interface RawCommonProps {
val actionCd: Long?
val actionDelay: Long?
val fastQuery: Boolean?
val matchRoot: Boolean?
val matchDelay: Long?
val matchTime: Long?
val actionMaximum: Int?
val resetMatch: String?
val actionCdKey: Int?
val actionMaximumKey: Int?
val order: Int?
val forcedTime: Long?
val snapshotUrls: List?
val excludeSnapshotUrls: List?
val exampleUrls: List?
val priorityTime: Long?
val priorityActionMaximum: Int?
}
sealed interface RawRuleProps : RawCommonProps {
val name: String?
val key: Int?
val preKeys: List?
val action: String?
val position: Position?
val matches: List?
val anyMatches: List?
val excludeMatches: List?
val excludeAllMatches: List?
fun getAllSelectorStrings(): List {
return listOfNotNull(matches, excludeMatches, anyMatches, excludeAllMatches).flatten()
}
}
sealed interface RawGroupProps : RawCommonProps {
val name: String
val key: Int
val desc: String?
val enable: Boolean?
val scopeKeys: List?
val rules: List
val valid: Boolean
val errorDesc: String?
val allExampleUrls: List
val cacheMap: MutableMap
val cacheStr: String
val cacheJsonObject: JsonObject
val groupType: Int
get() = when (this) {
is RawAppGroup -> SubsConfig.AppGroupType
is RawGlobalGroup -> SubsConfig.GlobalGroupType
}
}
sealed interface RawAppRuleProps {
val activityIds: List?
val excludeActivityIds: List?
val versionCode: IntegerMatcher?
val versionName: StringMatcher?
}
sealed interface RawGlobalRuleProps {
val matchAnyApp: Boolean?
val matchSystemApp: Boolean?
val matchLauncher: Boolean?
val apps: List?
}
@Serializable
data class RawGlobalApp(
val id: String,
val enable: Boolean?,
override val activityIds: List?,
override val excludeActivityIds: List?,
override val versionCode: IntegerMatcher?,
override val versionName: StringMatcher?,
) : RawAppRuleProps
@Serializable
data class RawGlobalGroup(
override val key: Int,
override val name: String,
override val desc: String?,
override val enable: Boolean?,
override val scopeKeys: List?,
override val actionCd: Long?,
override val actionDelay: Long?,
override val fastQuery: Boolean?,
override val matchRoot: Boolean?,
override val matchDelay: Long?,
override val matchTime: Long?,
override val actionMaximum: Int?,
override val resetMatch: String?,
override val actionCdKey: Int?,
override val actionMaximumKey: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?,
override val forcedTime: Long?,
override val snapshotUrls: List?,
override val excludeSnapshotUrls: List?,
override val exampleUrls: List?,
override val matchAnyApp: Boolean?,
override val matchSystemApp: Boolean?,
override val matchLauncher: Boolean?,
val disableIfAppGroupMatch: String?,
override val rules: List,
override val apps: List?,
) : RawGroupProps, RawGlobalRuleProps {
val appIdEnable: Map by lazy {
if (rules.all { r -> r.apps.isNullOrEmpty() }) {
apps?.associate { a -> a.id to (a.enable ?: true) } ?: emptyMap()
} else {
val allIds = mutableSetOf()
apps?.forEach { a ->
allIds.add(a.id)
}
rules.forEach { r ->
r.apps?.forEach { a ->
allIds.add(a.id)
}
}
val dataMap = mutableMapOf()
allIds.forEach forEachId@{ id ->
var temp: Boolean? = null
rules.forEach { r ->
val v = (r.apps ?: apps)?.find { it.id == id }?.enable ?: return@forEachId
if (temp == null) {
temp = v
} else if (temp != v) {
return@forEachId
}
}
if (temp != null) {
dataMap[id] = temp
}
}
dataMap
}
}
override val cacheMap by lazy { HashMap() }
override val errorDesc by lazy { getErrorDesc() }
override val valid by lazy { errorDesc == null }
override val allExampleUrls by lazy {
((exampleUrls ?: emptyList()) + rules.flatMap { r ->
r.exampleUrls ?: emptyList()
}).distinct()
}
override val cacheStr by lazy { toJson5String(this) }
override val cacheJsonObject by lazy { json.encodeToJsonElement(this).jsonObject }
}
@Serializable
data class RawGlobalRule(
override val key: Int?,
override val name: String?,
override val actionCd: Long?,
override val actionDelay: Long?,
override val fastQuery: Boolean?,
override val matchRoot: Boolean?,
override val matchDelay: Long?,
override val matchTime: Long?,
override val actionMaximum: Int?,
override val resetMatch: String?,
override val actionCdKey: Int?,
override val actionMaximumKey: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?,
override val forcedTime: Long?,
override val snapshotUrls: List?,
override val excludeSnapshotUrls: List?,
override val exampleUrls: List?,
override val preKeys: List?,
override val action: String?,
override val position: Position?,
override val matches: List?,
override val excludeMatches: List?,
override val excludeAllMatches: List?,
override val anyMatches: List?,
override val matchAnyApp: Boolean?,
override val matchSystemApp: Boolean?,
override val matchLauncher: Boolean?,
override val apps: List?
) : RawRuleProps, RawGlobalRuleProps
@Serializable
data class RawAppGroup(
override val key: Int,
override val name: String,
override val desc: String?,
override val enable: Boolean?,
override val scopeKeys: List?,
override val actionCdKey: Int?,
override val actionMaximumKey: Int?,
override val actionCd: Long?,
override val actionDelay: Long?,
override val fastQuery: Boolean?,
override val matchRoot: Boolean?,
override val actionMaximum: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?,
override val forcedTime: Long?,
override val matchDelay: Long?,
override val matchTime: Long?,
override val resetMatch: String?,
override val snapshotUrls: List?,
override val excludeSnapshotUrls: List?,
override val exampleUrls: List?,
override val activityIds: List?,
override val excludeActivityIds: List?,
override val rules: List,
override val versionCode: IntegerMatcher?,
override val versionName: StringMatcher?,
val ignoreGlobalGroupMatch: Boolean?,
) : RawGroupProps, RawAppRuleProps {
override val cacheMap by lazy { HashMap() }
override val errorDesc by lazy { getErrorDesc() }
override val valid by lazy { errorDesc == null }
override val allExampleUrls by lazy {
((exampleUrls ?: emptyList()) + rules.flatMap { r ->
r.exampleUrls ?: emptyList()
}).distinct()
}
override val cacheStr by lazy { toJson5String(this) }
override val cacheJsonObject by lazy { json.encodeToJsonElement(this).jsonObject }
}
@Serializable
data class RawAppRule(
override val key: Int?,
override val name: String?,
override val preKeys: List?,
override val action: String?,
override val position: Position?,
override val matches: List?,
override val excludeMatches: List?,
override val excludeAllMatches: List?,
override val anyMatches: List?,
override val actionCdKey: Int?,
override val actionMaximumKey: Int?,
override val actionCd: Long?,
override val actionDelay: Long?,
override val fastQuery: Boolean?,
override val matchRoot: Boolean?,
override val actionMaximum: Int?,
override val priorityTime: Long?,
override val priorityActionMaximum: Int?,
override val order: Int?,
override val forcedTime: Long?,
override val matchDelay: Long?,
override val matchTime: Long?,
override val resetMatch: String?,
override val snapshotUrls: List?,
override val excludeSnapshotUrls: List?,
override val exampleUrls: List?,
override val activityIds: List?,
override val excludeActivityIds: List?,
override val versionCode: IntegerMatcher?,
override val versionName: StringMatcher?,
) : RawRuleProps, RawAppRuleProps
companion object {
private fun RawGroupProps.getErrorDesc(): String? {
val allSelectorStrings = rules.map { r ->
r.getAllSelectorStrings()
}.flatten()
allSelectorStrings.forEach { source ->
try {
val selector = Selector.parse(source)
selector.checkType(typeInfo)
cacheMap[source] = selector
} catch (e: Exception) {
LogUtils.d("非法选择器", source, e.toString())
return "非法选择器\n$source\n${e.message}"
}
}
rules.forEach { r ->
if (r.position?.isValid == false) {
return "非法位置:${r.position}"
}
}
return null
}
private val expVars = arrayOf(
"left",
"top",
"right",
"bottom",
"width",
"height",
"random"
)
private fun setVariables(exp: Expression, rect: Rect) {
exp.setVariable("left", rect.left.toDouble())
exp.setVariable("top", rect.top.toDouble())
exp.setVariable("right", rect.right.toDouble())
exp.setVariable("bottom", rect.bottom.toDouble())
exp.setVariable("width", rect.width().toDouble())
exp.setVariable("height", rect.height().toDouble())
exp.setVariable("random", Math.random())
}
private fun getExpression(value: String?): Expression? {
return if (value != null) {
try {
ExpressionBuilder(value).variables(*expVars).build().apply {
expVars.forEach { v ->
// 预填充作 validate
setVariable(v, 0.0)
}
}.let { e ->
if (e.validate().isValid) {
e
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
} else {
null
}
}
private fun getPosition(jsonObject: JsonObject?): Position? {
return when (val element = jsonObject?.get("position")) {
JsonNull, null -> null
is JsonObject -> {
Position(
left = element["left"]?.jsonPrimitive?.content,
bottom = element["bottom"]?.jsonPrimitive?.content,
top = element["top"]?.jsonPrimitive?.content,
right = element["right"]?.jsonPrimitive?.content,
)
}
else -> null
}
}
private fun getStringIArray(jsonObject: JsonObject?, name: String): List? {
return when (val element = jsonObject?.get(name)) {
JsonNull, null -> null
is JsonObject -> error("Element ${this::class} can not be object")
is JsonArray -> element.map {
when (it) {
is JsonObject, is JsonArray, JsonNull -> error("Element ${this::class} is not a int")
is JsonPrimitive -> it.content
}
}
is JsonPrimitive -> listOf(element.content)
}
}
@Suppress("SameParameterValue")
private fun getIntMatcher(jsonObject: JsonObject?, key: String): IntegerMatcher? {
return when (val element = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonObject -> IntegerMatcher(
minimum = getInt(element, "minimum"),
maximum = getInt(element, "maximum"),
include = getIntIArray(element, "include"),
exclude = getIntIArray(element, "exclude"),
)
else -> error("Element $element is not a IntMatcher")
}
}
@Suppress("SameParameterValue")
private fun getStringMatcher(jsonObject: JsonObject?, key: String): StringMatcher? {
return when (val element = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonObject -> StringMatcher(
pattern = getString(element, "pattern"),
include = getStringIArray(element, "include"),
exclude = getStringIArray(element, "exclude"),
)
else -> error("Element $element is not a StringMatcher")
}
}
private fun getIntIArray(jsonObject: JsonObject?, key: String): List? {
return when (val element = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonArray -> element.map {
when (it) {
is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
is JsonPrimitive -> it.int
}
}
is JsonPrimitive -> listOf(element.int)
else -> error("Element $element is not a Array")
}
}
private fun getString(jsonObject: JsonObject?, key: String): String? =
when (val p = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonPrimitive -> {
if (p.isString) {
p.content
} else {
null
}
}
else -> null
}
private fun getLong(jsonObject: JsonObject?, key: String): Long? =
when (val p = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonPrimitive -> {
p.long
}
else -> null
}
private fun getInt(jsonObject: JsonObject?, key: String): Int? =
when (val p = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonPrimitive -> {
p.int
}
else -> null
}
private fun getBoolean(jsonObject: JsonObject?, key: String): Boolean? =
when (val p = jsonObject?.get(key)) {
JsonNull, null -> null
is JsonPrimitive -> {
p.boolean
}
else -> null
}
private fun getCompatVersionCode(jsonObject: JsonObject): IntegerMatcher? {
getIntMatcher(jsonObject, "versionCode")?.let { return it }
// compat old value
val a = getIntIArray(jsonObject, "versionCodes")
val b = getIntIArray(jsonObject, "excludeVersionCodes")
if (a != null || b != null) {
return IntegerMatcher(
minimum = null,
maximum = null,
include = a,
exclude = b
)
}
return null
}
private fun getCompatVersionName(jsonObject: JsonObject): StringMatcher? {
getStringMatcher(jsonObject, "versionName")?.let { return it }
// compat old value
val a = getStringIArray(jsonObject, "versionNames")
val b = getStringIArray(jsonObject, "excludeVersionNames")
if (a != null || b != null) {
return StringMatcher(
pattern = null,
include = a,
exclude = b
)
}
return null
}
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RawAppRule {
val jsonObject = when (rulesRawJson) {
JsonNull -> error("miss current rule")
is JsonObject -> rulesRawJson
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("matches" to rulesRawJson))
}
return RawAppRule(
activityIds = getStringIArray(jsonObject, "activityIds"),
excludeActivityIds = getStringIArray(jsonObject, "excludeActivityIds"),
matches = getStringIArray(jsonObject, "matches"),
excludeMatches = getStringIArray(jsonObject, "excludeMatches"),
excludeAllMatches = getStringIArray(jsonObject, "excludeAllMatches"),
anyMatches = getStringIArray(jsonObject, "anyMatches"),
key = getInt(jsonObject, "key"),
name = getString(jsonObject, "name"),
actionCd = getLong(jsonObject, "actionCd") ?: getLong(jsonObject, "cd"),
actionDelay = getLong(jsonObject, "actionDelay") ?: getLong(jsonObject, "delay"),
preKeys = getIntIArray(jsonObject, "preKeys"),
action = getString(jsonObject, "action"),
fastQuery = getBoolean(jsonObject, "fastQuery"),
matchRoot = getBoolean(jsonObject, "matchRoot"),
actionMaximum = getInt(jsonObject, "actionMaximum"),
matchDelay = getLong(jsonObject, "matchDelay"),
matchTime = getLong(jsonObject, "matchTime"),
resetMatch = getString(jsonObject, "resetMatch"),
snapshotUrls = getStringIArray(jsonObject, "snapshotUrls"),
excludeSnapshotUrls = getStringIArray(jsonObject, "excludeSnapshotUrls"),
exampleUrls = getStringIArray(jsonObject, "exampleUrls"),
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
actionCdKey = getInt(jsonObject, "actionCdKey"),
order = getInt(jsonObject, "order"),
versionCode = getCompatVersionCode(jsonObject),
versionName = getCompatVersionName(jsonObject),
position = getPosition(jsonObject),
forcedTime = getLong(jsonObject, "forcedTime"),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
)
}
private fun jsonToGroupRaw(groupRawJson: JsonElement): RawAppGroup {
val jsonObject = when (groupRawJson) {
JsonNull -> error("group must not be null")
is JsonObject -> groupRawJson
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupRawJson))
}
return RawAppGroup(
activityIds = getStringIArray(jsonObject, "activityIds"),
excludeActivityIds = getStringIArray(jsonObject, "excludeActivityIds"),
actionCd = getLong(jsonObject, "actionCd") ?: getLong(jsonObject, "cd"),
actionDelay = getLong(jsonObject, "actionDelay") ?: getLong(jsonObject, "delay"),
name = getString(jsonObject, "name") ?: error("miss group name"),
desc = getString(jsonObject, "desc"),
enable = getBoolean(jsonObject, "enable"),
key = getInt(jsonObject, "key") ?: error("miss group key"),
rules = when (val rulesJson = jsonObject["rules"]) {
null, JsonNull -> emptyList()
is JsonPrimitive, is JsonObject -> JsonArray(listOf(rulesJson))
is JsonArray -> rulesJson
}.map {
jsonToRuleRaw(it)
}.distinctNotNullBy { it.key },
fastQuery = getBoolean(jsonObject, "fastQuery"),
matchRoot = getBoolean(jsonObject, "matchRoot"),
actionMaximum = getInt(jsonObject, "actionMaximum"),
matchDelay = getLong(jsonObject, "matchDelay"),
matchTime = getLong(jsonObject, "matchTime"),
resetMatch = getString(jsonObject, "resetMatch"),
snapshotUrls = getStringIArray(jsonObject, "snapshotUrls"),
excludeSnapshotUrls = getStringIArray(jsonObject, "excludeSnapshotUrls"),
exampleUrls = getStringIArray(jsonObject, "exampleUrls"),
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
actionCdKey = getInt(jsonObject, "actionCdKey"),
order = getInt(jsonObject, "order"),
forcedTime = getLong(jsonObject, "forcedTime"),
scopeKeys = getIntIArray(jsonObject, "scopeKeys"),
versionCode = getCompatVersionCode(jsonObject),
versionName = getCompatVersionName(jsonObject),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
ignoreGlobalGroupMatch = getBoolean(jsonObject, "ignoreGlobalGroupMatch"),
)
}
private fun jsonToAppRaw(jsonObject: JsonObject, appIndex: Int? = null): RawApp {
return RawApp(
id = getString(jsonObject, "id") ?: error(
if (appIndex != null) {
"miss subscription.apps[$appIndex].id"
} else {
"miss id"
}
),
name = getString(jsonObject, "name"),
groups = (when (val groupsJson = jsonObject["groups"]) {
null, JsonNull -> emptyList()
is JsonPrimitive, is JsonObject -> JsonArray(listOf(groupsJson))
is JsonArray -> groupsJson
}).map { jsonElement ->
jsonToGroupRaw(jsonElement)
}.distinctByIfAny { it.key },
)
}
private fun jsonToGlobalApp(jsonObject: JsonObject, index: Int): RawGlobalApp {
return RawGlobalApp(
id = getString(jsonObject, "id") ?: error("miss apps[$index].id"),
enable = getBoolean(jsonObject, "enable"),
activityIds = getStringIArray(jsonObject, "activityIds"),
excludeActivityIds = getStringIArray(jsonObject, "excludeActivityIds"),
versionCode = getCompatVersionCode(jsonObject),
versionName = getCompatVersionName(jsonObject),
)
}
private fun jsonToGlobalRule(jsonObject: JsonObject): RawGlobalRule {
return RawGlobalRule(
key = getInt(jsonObject, "key"),
name = getString(jsonObject, "name"),
actionCd = getLong(jsonObject, "actionCd"),
actionDelay = getLong(jsonObject, "actionDelay"),
fastQuery = getBoolean(jsonObject, "fastQuery"),
matchRoot = getBoolean(jsonObject, "matchRoot"),
actionMaximum = getInt(jsonObject, "actionMaximum"),
matchDelay = getLong(jsonObject, "matchDelay"),
matchTime = getLong(jsonObject, "matchTime"),
resetMatch = getString(jsonObject, "resetMatch"),
snapshotUrls = getStringIArray(jsonObject, "snapshotUrls"),
excludeSnapshotUrls = getStringIArray(jsonObject, "excludeSnapshotUrls"),
exampleUrls = getStringIArray(jsonObject, "exampleUrls"),
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
actionCdKey = getInt(jsonObject, "actionCdKey"),
matchAnyApp = getBoolean(jsonObject, "matchAnyApp"),
matchSystemApp = getBoolean(jsonObject, "matchSystemApp"),
matchLauncher = getBoolean(jsonObject, "matchLauncher"),
apps = jsonObject["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
jsonToGlobalApp(
jsonElement.jsonObject, index
)
}?.distinctByIfAny { it.id },
action = getString(jsonObject, "action"),
preKeys = getIntIArray(jsonObject, "preKeys"),
excludeMatches = getStringIArray(jsonObject, "excludeMatches"),
excludeAllMatches = getStringIArray(jsonObject, "excludeAllMatches"),
matches = getStringIArray(jsonObject, "matches"),
anyMatches = getStringIArray(jsonObject, "anyMatches"),
order = getInt(jsonObject, "order"),
forcedTime = getLong(jsonObject, "forcedTime"),
position = getPosition(jsonObject),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
)
}
private fun jsonToGlobalGroup(jsonObject: JsonObject, groupIndex: Int): RawGlobalGroup {
return RawGlobalGroup(
key = getInt(jsonObject, "key") ?: error("miss group[$groupIndex].key"),
name = getString(jsonObject, "name") ?: error("miss group[$groupIndex].name"),
desc = getString(jsonObject, "desc"),
enable = getBoolean(jsonObject, "enable"),
actionCd = getLong(jsonObject, "actionCd"),
actionDelay = getLong(jsonObject, "actionDelay"),
fastQuery = getBoolean(jsonObject, "fastQuery"),
matchRoot = getBoolean(jsonObject, "matchRoot"),
actionMaximum = getInt(jsonObject, "actionMaximum"),
matchDelay = getLong(jsonObject, "matchDelay"),
matchTime = getLong(jsonObject, "matchTime"),
resetMatch = getString(jsonObject, "resetMatch"),
snapshotUrls = getStringIArray(jsonObject, "snapshotUrls"),
excludeSnapshotUrls = getStringIArray(jsonObject, "excludeSnapshotUrls"),
exampleUrls = getStringIArray(jsonObject, "exampleUrls"),
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
actionCdKey = getInt(jsonObject, "actionCdKey"),
matchSystemApp = getBoolean(jsonObject, "matchSystemApp"),
matchAnyApp = getBoolean(jsonObject, "matchAnyApp"),
matchLauncher = getBoolean(jsonObject, "matchLauncher"),
apps = jsonObject["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
jsonToGlobalApp(
jsonElement.jsonObject, index
)
}?.distinctByIfAny { it.id },
rules = (jsonObject["rules"]?.jsonArray?.map { jsonElement ->
jsonToGlobalRule(jsonElement.jsonObject)
} ?: emptyList()).distinctNotNullBy { it.key },
order = getInt(jsonObject, "order"),
scopeKeys = getIntIArray(jsonObject, "scopeKeys"),
forcedTime = getLong(jsonObject, "forcedTime"),
priorityTime = getLong(jsonObject, "priorityTime"),
priorityActionMaximum = getInt(jsonObject, "priorityActionMaximum"),
disableIfAppGroupMatch = getString(jsonObject, "disableIfAppGroupMatch"),
)
}
private fun jsonToSubscriptionRaw(rootJson: JsonObject): RawSubscription {
return RawSubscription(
id = getLong(rootJson, "id") ?: error("miss subscription.id"),
name = getString(rootJson, "name") ?: error("miss subscription.name"),
version = getInt(rootJson, "version") ?: error("miss subscription.version"),
author = getString(rootJson, "author"),
updateUrl = getString(rootJson, "updateUrl"),
supportUri = getString(rootJson, "supportUri"),
checkUpdateUrl = getString(rootJson, "checkUpdateUrl"),
apps = (rootJson["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
jsonToAppRaw(
jsonElement.jsonObject,
index
)
} ?: emptyList()).filterIfNotAll { it.groups.isNotEmpty() }
.distinctByIfAny { it.id },
categories = (rootJson["categories"]?.jsonArray?.mapIndexed { index, jsonElement ->
RawCategory(
key = getInt(jsonElement.jsonObject, "key")
?: error("miss categories[$index].key"),
name = getString(jsonElement.jsonObject, "name")
?: error("miss categories[$index].name"),
enable = getBoolean(jsonElement.jsonObject, "enable"),
desc = getString(jsonElement.jsonObject, "desc")
)
} ?: emptyList()).filterIfNotAll { it.name.isNotEmpty() }
.distinctByIfAny { it.key },
globalGroups = (rootJson["globalGroups"]?.jsonArray?.mapIndexed { index, jsonElement ->
jsonToGlobalGroup(jsonElement.jsonObject, index)
} ?: emptyList()).distinctByIfAny { it.key }
)
}
private fun List.distinctNotNullBy(selector: (T) -> Any?): List {
val set = HashSet()
val list = ArrayList()
forEach { e ->
val key = selector(e)
if (key == null || set.add(key)) {
list.add(e)
}
}
return list
}
fun parse(source: String, json5: Boolean = true): RawSubscription {
val element = if (json5) {
Json5.parseToJson5Element(source)
} else {
json.parseToJsonElement(source)
}
return jsonToSubscriptionRaw(element.jsonObject)
}
fun parseApp(jsonObject: JsonObject): RawApp {
return jsonToAppRaw(jsonObject)
}
fun parseAppGroup(jsonObject: JsonObject): RawAppGroup {
return jsonToGroupRaw(jsonObject)
}
fun parseGlobalGroup(jsonObject: JsonObject): RawGlobalGroup {
return jsonToGlobalGroup(jsonObject, 0)
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/ResolvedGroup.kt
================================================
package li.songe.gkd.data
sealed class ResolvedGroup(
open val group: RawSubscription.RawGroupProps,
val subscription: RawSubscription,
val subsItem: SubsItem,
val config: SubsConfig?,
) {
val excludeData by lazy { ExcludeData.parse(config?.exclude) }
abstract val appId: String?
}
class ResolvedAppGroup(
override val group: RawSubscription.RawAppGroup,
subscription: RawSubscription,
subsItem: SubsItem,
config: SubsConfig?,
val app: RawSubscription.RawApp,
val enable: Boolean,
) : ResolvedGroup(group, subscription, subsItem, config) {
override val appId: String?
get() = app.id
}
class ResolvedGlobalGroup(
override val group: RawSubscription.RawGlobalGroup,
subscription: RawSubscription,
subsItem: SubsItem,
config: SubsConfig?,
) : ResolvedGroup(group, subscription, subsItem, config) {
override val appId: String?
get() = null
val groupExcludeAppIds by lazy {
subscription.globalGroupAppGroupNameDisableMap[group.key] ?: emptySet()
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt
================================================
package li.songe.gkd.data
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.updateAndGet
import li.songe.gkd.a11y.appChangeTime
import li.songe.gkd.a11y.lastTriggerRule
import li.songe.gkd.a11y.lastTriggerTime
import li.songe.gkd.store.actionCountFlow
import li.songe.selector.MatchOption
import li.songe.selector.Selector
sealed class ResolvedRule(
val rule: RawSubscription.RawRuleProps,
val g: ResolvedGroup,
) {
private val group = g.group
val subsItem = g.subsItem
val rawSubs = g.subscription
val key = rule.key
val index = group.rules.indexOfFirst { r -> r === rule }
val excludeData = g.excludeData
private val preKeys = (rule.preKeys ?: emptyList()).toSet()
val matches =
(rule.matches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }
val anyMatches =
(rule.anyMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }
val excludeMatches =
(rule.excludeMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }
val excludeAllMatches =
(rule.excludeAllMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }
private val resetMatch = rule.resetMatch ?: group.resetMatch
val matchDelay = rule.matchDelay ?: group.matchDelay ?: 0L
val actionDelay = rule.actionDelay ?: group.actionDelay ?: 0L
private val matchTime = rule.matchTime ?: group.matchTime
private val forcedTime = rule.forcedTime ?: group.forcedTime ?: 0L
val matchOption = MatchOption(
fastQuery = rule.fastQuery ?: group.fastQuery ?: false
)
val matchRoot = rule.matchRoot ?: group.matchRoot ?: false
val order = rule.order ?: group.order ?: 0
private val actionCdKey = rule.actionCdKey ?: group.actionCdKey
private val actionCd = rule.actionCd ?: if (actionCdKey != null) {
group.rules.find { r -> r.key == actionCdKey }?.actionCd
} else {
null
} ?: group.actionCd ?: 1000L
private val actionMaximumKey = rule.actionMaximumKey ?: group.actionMaximumKey
private val actionMaximum = rule.actionMaximum ?: if (actionMaximumKey != null) {
group.rules.find { r -> r.key == actionMaximumKey }?.actionMaximum
} else {
null
} ?: group.actionMaximum
private val hasSlowSelector by lazy {
(matches + excludeMatches + anyMatches + excludeAllMatches).any { s -> s.isSlow(matchOption) }
}
val priorityTime = rule.priorityTime ?: group.priorityTime ?: 0
val priorityActionMaximum = rule.priorityActionMaximum ?: group.priorityActionMaximum ?: 1
val priorityEnabled: Boolean
get() = priorityTime > 0
fun isPriority(): Boolean {
if (!priorityEnabled) return false
if (priorityActionMaximum <= actionCount.value) return false
if (!status.ok) return false
val t = System.currentTimeMillis()
return t - matchChangedTime.value < priorityTime + matchDelay
}
val isSlow by lazy { preKeys.isEmpty() && (matchTime == null || matchTime > 10_000L) && hasSlowSelector }
var groupToRules: Map> = emptyMap()
set(value) {
field = value
val selfGroupRules = field[group] ?: emptyList()
val othersGroupRules =
(group.scopeKeys ?: emptyList()).distinct().filter { k -> k != group.key }
.flatMap { k ->
field.entries.find { e -> e.key.key == k }?.value ?: emptyList()
}
val groupRules = selfGroupRules + othersGroupRules
// 共享次数
if (actionMaximumKey != null) {
val otherRule = groupRules.find { r -> r.key == actionMaximumKey }
if (otherRule != null) {
actionCount = otherRule.actionCount
}
}
// 共享 cd
if (actionCdKey != null) {
val otherRule = groupRules.find { r -> r.key == actionCdKey }
if (otherRule != null) {
actionTriggerTime = otherRule.actionTriggerTime
}
}
preRules = groupRules.filter { otherRule ->
(otherRule.key != null) && preKeys.contains(
otherRule.key
)
}.toSet()
}
private var preRules = emptySet()
val hasNext = group.rules.any { r -> r.preKeys?.any { k -> k == rule.key } == true }
private var actionDelayTriggerTime = atomic(0L)
val actionDelayJob = atomic(null)
fun checkDelay(): Boolean {
if (actionDelay > 0 && actionDelayTriggerTime.value == 0L) {
actionDelayTriggerTime.value = System.currentTimeMillis()
return true
}
return false
}
fun checkForced(): Boolean {
if (forcedTime <= 0) return false
return System.currentTimeMillis() < matchChangedTime.value + matchDelay + forcedTime
}
private var actionTriggerTime = atomic(0L)
fun trigger() {
val t = System.currentTimeMillis()
actionTriggerTime.value = t
actionDelayTriggerTime.value = 0L
actionCount.incrementAndGet()
lastTriggerTime = t
lastTriggerRule = this
actionCountFlow.updateAndGet { it + 1 }
}
private var actionCount = atomic(0)
private val matchChangedTime = atomic(0L)
val isFirstMatchApp: Boolean
get() = matchChangedTime.value < appChangeTime
private val matchLimitTime = (matchTime ?: 0) + matchDelay
val resetMatchType = ResetMatchType.allSubObject.find {
it.value == resetMatch
} ?: ResetMatchType.Activity
fun resetState(t: Long) {
actionCount.value = 0
actionDelayTriggerTime.value = 0L
actionTriggerTime.value = 0
actionDelayJob.update { it?.cancel(); null }
matchDelayJob.update { it?.cancel(); null }
matchChangedTime.value = t
}
private val performer = ActionPerformer.getAction(rule.action ?: rule.position?.let {
ActionPerformer.ClickCenter.action
})
fun performAction(node: AccessibilityNodeInfo): ActionResult {
return performer.perform(node, rule.position)
}
val matchDelayJob = atomic(null)
val status: RuleStatus
get() {
if (actionMaximum != null) {
if (actionCount.value >= actionMaximum) {
return RuleStatus.Status1 // 达到最大执行次数
}
}
if (preRules.isNotEmpty() && !preRules.any { it === lastTriggerRule }) {
return RuleStatus.Status2 // 需要提前触发某个规则
}
val t = System.currentTimeMillis()
val c = matchChangedTime.value
if (matchDelay > 0 && t - c < matchDelay) {
return RuleStatus.Status3 // 处于匹配延迟中
}
if (matchTime != null && t - c > matchLimitTime) {
return RuleStatus.Status4 // 超出匹配时间
}
if (actionTriggerTime.value + actionCd > t) {
return RuleStatus.Status5 // 处于冷却时间
}
val d = actionDelayTriggerTime.value
if (d > 0) {
if (d + actionDelay > t) {
return RuleStatus.Status6 // 处于触发延迟中
}
}
return RuleStatus.StatusOk
}
fun statusText(): String {
return "id:${subsItem.id}, v:${rawSubs.version}, type:${type}, gKey=${group.key}, gName:${group.name}, index:${index}, key:${key}, status:${status.name}"
}
abstract val type: String
// 范围越精确, 优先级越高
abstract fun matchActivity(appId: String, activityId: String? = null): Boolean
}
sealed class ResetMatchType(val value: String) {
data object Activity : ResetMatchType("activity")
data object Match : ResetMatchType("match")
data object App : ResetMatchType("app")
companion object {
val allSubObject by lazy { listOf(Activity, Match, App) }
}
}
sealed class RuleStatus(val name: String) {
data object StatusOk : RuleStatus("ok")
data object Status1 : RuleStatus("达到最大执行次数")
data object Status2 : RuleStatus("需要提前触发某个规则")
data object Status3 : RuleStatus("处于匹配延迟")
data object Status4 : RuleStatus("超出匹配时间")
data object Status5 : RuleStatus("处于冷却时间")
data object Status6 : RuleStatus("处于触发延迟")
val ok: Boolean
get() = this === StatusOk
val alive: Boolean
get() = this !== Status1 && this !== Status2 && this !== Status4
}
fun getFixActivityIds(
appId: String,
activityIds: List?,
): List {
if (activityIds.isNullOrEmpty()) return emptyList()
return activityIds.map { activityId ->
if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
appId + activityId
} else {
activityId
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/RpcError.kt
================================================
package li.songe.gkd.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RpcError(
override val message: String,
@SerialName("__error") val error: Boolean = true,
val unknown: Boolean = false,
) : Exception(message)
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/Snapshot.kt
================================================
package li.songe.gkd.data
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import li.songe.gkd.util.SnapshotExt
import li.songe.gkd.util.format
@Entity(
tableName = "snapshot",
)
@Serializable
data class Snapshot(
@PrimaryKey @ColumnInfo(name = "id") override val id: Long,
@ColumnInfo(name = "app_id") override val appId: String,
@ColumnInfo(name = "activity_id") override val activityId: String?,
@ColumnInfo(name = "screen_height") override val screenHeight: Int,
@ColumnInfo(name = "screen_width") override val screenWidth: Int,
@ColumnInfo(name = "is_landscape") override val isLandscape: Boolean,
@ColumnInfo(name = "github_asset_id") val githubAssetId: Int? = null,
) : BaseSnapshot {
val date by lazy { id.format("MM-dd HH:mm:ss") }
val screenshotFile by lazy { SnapshotExt.screenshotFile(id) }
@Dao
interface SnapshotDao {
@Update
suspend fun update(vararg objects: Snapshot): Int
@Insert
suspend fun insert(vararg users: Snapshot): List
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(vararg users: Snapshot): List
@Query("DELETE FROM snapshot")
suspend fun deleteAll()
@Delete
suspend fun delete(vararg users: Snapshot): Int
@Query("SELECT * FROM snapshot ORDER BY id DESC")
fun query(): Flow>
@Query("UPDATE snapshot SET github_asset_id=null WHERE id = :id")
suspend fun deleteGithubAssetId(id: Long)
@Query("SELECT COUNT(*) FROM snapshot")
fun count(): Flow
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/SubsConfig.kt
================================================
package li.songe.gkd.data
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import li.songe.gkd.util.isValidActivityId
import li.songe.gkd.util.isValidAppId
private var lastId = 0L
@Synchronized
private fun buildUniqueTimeMillisId(): Long {
val id = System.currentTimeMillis()
if (id > lastId) {
lastId = id
} else {
lastId += 1
}
return lastId
}
@Serializable
@Entity(
tableName = "subs_config",
)
data class SubsConfig(
@PrimaryKey @ColumnInfo(name = "id") val id: Long = buildUniqueTimeMillisId(),
@ColumnInfo(name = "type") val type: Int,
@ColumnInfo(name = "enable") val enable: Boolean? = null,
@ColumnInfo(name = "subs_id") val subsId: Long,
@ColumnInfo(name = "app_id") val appId: String = "",
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
@ColumnInfo(name = "exclude", defaultValue = "") val exclude: String = "",
) {
@Suppress("ConstPropertyName")
companion object {
const val AppGroupType = 2
const val GlobalGroupType = 3
}
@Dao
interface SubsConfigDao {
@Update
suspend fun update(vararg objects: SubsConfig): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg users: SubsConfig): List
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(vararg users: SubsConfig): List
@Delete
suspend fun delete(vararg users: SubsConfig): Int
@Transaction
suspend fun insertAndDelete(newList: List, deleteList: List) {
insert(*newList.toTypedArray())
delete(*deleteList.toTypedArray())
}
@Query("DELETE FROM subs_config WHERE subs_id=:subsItemId")
suspend fun delete(subsItemId: Long): Int
@Query("DELETE FROM subs_config WHERE subs_id IN (:subsIds)")
suspend fun deleteBySubsId(vararg subsIds: Long): Int
@Query("DELETE FROM subs_config WHERE subs_id=:subsItemId AND app_id=:appId")
suspend fun deleteAppConfig(subsItemId: Long, appId: String): Int
@Query("DELETE FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key=:groupKey")
suspend fun deleteAppGroupConfig(subsItemId: Long, appId: String, groupKey: Int): Int
@Query("DELETE FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key IN (:keyList)")
suspend fun batchDeleteAppGroupConfig(
subsItemId: Long,
appId: String,
keyList: List
): Int
@Query("DELETE FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId AND group_key=:groupKey")
suspend fun deleteGlobalGroupConfig(subsItemId: Long, groupKey: Int): Int
@Query("DELETE FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId AND group_key IN (:keyList)")
suspend fun batchDeleteGlobalGroupConfig(subsItemId: Long, keyList: List): Int
@Query("SELECT * FROM subs_config WHERE subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)")
fun queryUsedList(): Flow>
@Query("SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId")
fun querySubsGroupTypeConfig(subsItemId: Long): Flow>
@Query("SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId")
fun queryAppGroupTypeConfig(subsItemId: Long, appId: String): Flow>
@Query("SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key=:groupKey")
fun queryAppGroupTypeConfig(
subsItemId: Long, appId: String, groupKey: Int
): Flow
@Query("SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId")
fun queryGlobalGroupTypeConfig(subsItemId: Long): Flow>
@Query("SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId AND group_key=:groupKey")
fun queryGlobalGroupTypeConfig(subsItemId: Long, groupKey: Int): Flow
@Query("SELECT * FROM subs_config WHERE type=${AppGroupType} AND app_id=:appId AND subs_id IN (:subsItemIds)")
fun queryAppConfig(subsItemIds: List, appId: String): Flow>
@Query("SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id IN (:subsItemIds)")
fun queryGlobalConfig(subsItemIds: List): Flow>
@Query("SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)")
fun queryUsedGlobalConfig(): Flow>
@Query("SELECT * FROM subs_config WHERE subs_id IN (:subsItemIds) ")
suspend fun querySubsItemConfig(subsItemIds: List): List
@Query("UPDATE subs_config SET enable = null WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key=:groupKey AND enable IS NOT NULL")
suspend fun resetAppGroupTypeEnable(subsItemId: Long, appId: String, groupKey: Int): Int
@Transaction
suspend fun batchResetAppGroupEnable(
subsItemId: Long,
list: List>
): List> {
return list.filter { (g, a) ->
resetAppGroupTypeEnable(subsItemId, a.id, g.key) > 0
}
}
}
}
data class ExcludeData(
val appIds: Map,
val activityIds: Set>,
) {
val excludeAppIds = appIds.entries.filter { e -> e.value }.map { e -> e.key }.toHashSet()
val includeAppIds = appIds.entries.filter { e -> !e.value }.map { e -> e.key }.toHashSet()
fun stringify(appId: String? = null): String {
return if (appId != null) {
activityIds.filter { e -> e.first == appId }.map { e -> e.second }.sorted()
.joinToString("\n\n")
} else {
(appIds.entries.map { e ->
if (e.value) {
e.key
} else {
"!${e.key}"
}
} + activityIds.map { e -> "${e.first}/${e.second}" }).sorted().joinToString("\n\n")
}
}
fun clear(appId: String): ExcludeData {
return copy(
appIds = appIds.toMutableMap().apply {
remove(appId)
},
)
}
fun switch(appId: String, activityId: String? = null): ExcludeData {
return if (activityId == null) {
copy(
appIds = appIds.toMutableMap().apply {
if (get(appId) != false) {
set(appId, false)
} else {
set(appId, true)
}
},
)
} else {
copy(activityIds = activityIds.toMutableSet().apply {
val e = appId to activityId
if (contains(e)) {
remove(e)
} else {
add(e)
}
})
}
}
companion object {
private val empty = ExcludeData(emptyMap(), emptySet())
fun parse(exclude: String?): ExcludeData {
if (exclude.isNullOrBlank()) {
return empty
}
val appIds = HashMap()
val activityIds = HashSet>()
exclude.split('\n')
.filter { it.isNotBlank() }
.forEach { s ->
if (s[0] == '!') {
val appId = s.substring(1)
if (appId.isValidAppId()) {
appIds[appId] = false
}
} else {
val a = s.split('/', limit = 2)
val appId = a[0]
if (appId.isValidAppId()) {
val activityId = a.getOrNull(1)
if (activityId != null) {
if (activityId.isValidActivityId()) {
activityIds.add(appId to activityId)
}
} else {
appIds[appId] = true
}
}
}
}
return ExcludeData(
appIds = appIds,
activityIds = activityIds,
)
}
fun parse(exclude: String?, appId: String): ExcludeData {
if (exclude.isNullOrBlank()) return empty
return parse(exclude.split('\n').joinToString("\n") { "$appId/$it" })
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt
================================================
package li.songe.gkd.data
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import li.songe.gkd.util.LOCAL_SUBS_IDS
import li.songe.gkd.util.format
@Serializable
@Entity(
tableName = "subs_item",
)
data class SubsItem(
@PrimaryKey @ColumnInfo(name = "id") val id: Long,
@ColumnInfo(name = "ctime") val ctime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "mtime") val mtime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "enable") val enable: Boolean = false,
@ColumnInfo(name = "enable_update") val enableUpdate: Boolean = true,
@ColumnInfo(name = "order") val order: Int,
@ColumnInfo(name = "update_url") val updateUrl: String? = null,
) {
val isLocal: Boolean
get() = LOCAL_SUBS_IDS.contains(id)
val mtimeStr by lazy { mtime.format("yyyy-MM-dd HH:mm:ss") }
@Dao
interface SubsItemDao {
@Update
suspend fun update(vararg objects: SubsItem): Int
@Query("UPDATE subs_item SET enable=:enable WHERE id=:id")
suspend fun updateEnable(id: Long, enable: Boolean): Int
@Query("UPDATE subs_item SET `order`=:order WHERE id=:id")
suspend fun updateOrder(id: Long, order: Int): Int
@Transaction
suspend fun batchUpdateOrder(subsItems: List) {
subsItems.forEach { subsItem ->
updateOrder(subsItem.id, subsItem.order)
}
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg users: SubsItem): List
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(vararg users: SubsItem): List
@Delete
suspend fun delete(vararg users: SubsItem): Int
@Query("UPDATE subs_item SET mtime=:mtime WHERE id=:id")
suspend fun updateMtime(id: Long, mtime: Long = System.currentTimeMillis()): Int
@Query("SELECT * FROM subs_item ORDER BY `order`")
fun query(): Flow>
@Query("SELECT * FROM subs_item ORDER BY `order`")
fun queryAll(): List
@Query("DELETE FROM subs_item WHERE id IN (:ids)")
suspend fun deleteById(vararg ids: Long): Int
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/SubsVersion.kt
================================================
package li.songe.gkd.data
import kotlinx.serialization.Serializable
@Serializable
data class SubsVersion(val id: Long, val version: Int)
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/TransferData.kt
================================================
package li.songe.gkd.data
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import li.songe.gkd.db.DbSet
import li.songe.gkd.util.LOCAL_SUBS_IDS
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.UriUtils
import li.songe.gkd.util.ZipUtils
import li.songe.gkd.util.checkSubsUpdate
import li.songe.gkd.util.createGkdTempDir
import li.songe.gkd.util.json
import li.songe.gkd.util.sharedDir
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.subsMapFlow
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubscription
import java.io.File
@Serializable
private data class TransferData(
val type: String = TYPE,
val ctime: Long = System.currentTimeMillis(),
val subsItems: List = emptyList(),
val subsConfigs: List = emptyList(),
val categoryConfigs: List = emptyList(),
val appConfigs: List = emptyList()
) {
companion object {
const val TYPE = "transfer_data"
}
}
private const val subsDirName = "files"
private suspend fun importTransferData(transferData: TransferData): Boolean {
val maxOrder = (subsItemsFlow.value.maxOfOrNull { it.order } ?: -1) + 1
val subsItems =
transferData.subsItems.filter { s -> s.id >= 0 || LOCAL_SUBS_IDS.contains(s.id) }
.mapIndexed { i, s ->
s.copy(order = maxOrder + i)
}
val hasNewSubsItem =
subsItems.any { newSubs -> newSubs.id >= 0 && subsItemsFlow.value.all { oldSubs -> oldSubs.id != newSubs.id } }
DbSet.subsItemDao.insertOrIgnore(*subsItems.toTypedArray())
DbSet.subsConfigDao.insertOrIgnore(*transferData.subsConfigs.toTypedArray())
DbSet.categoryConfigDao.insertOrIgnore(*transferData.categoryConfigs.toTypedArray())
DbSet.appConfigDao.insertOrIgnore(*transferData.appConfigs.toTypedArray())
return hasNewSubsItem
}
suspend fun exportData(subsIds: Collection): File {
val tempDir = createGkdTempDir()
val dataFile = tempDir.resolve("${TransferData.TYPE}.json")
dataFile.writeText(
json.encodeToString(
TransferData(
subsItems = subsItemsFlow.value.filter { subsIds.contains(it.id) },
subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsIds.toList()),
categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsIds.toList()),
appConfigs = DbSet.appConfigDao.querySubsItemConfig(subsIds.toList()),
)
)
)
val localSubsList = subsMapFlow.value.values.filter {
it.id < 0 && subsIds.contains(it.id) && !it.isEmpty
}
val files = if (localSubsList.isNotEmpty()) {
val f = tempDir.resolve(subsDirName).apply { mkdir() }
localSubsList.forEach {
val file = f.resolve("${it.id}.json")
file.writeText(json.encodeToString(it))
}
f
} else {
null
}
val file = sharedDir.resolve("backup-${System.currentTimeMillis()}.zip")
ZipUtils.zipFiles(listOfNotNull(dataFile, files), file)
tempDir.deleteRecursively()
return file
}
suspend fun importData(uri: Uri) {
val tempDir = createGkdTempDir()
val zipFile = tempDir.resolve("file.zip").apply {
writeBytes(UriUtils.uri2Bytes(uri))
}
val unzipDir = tempDir.resolve("unzip").apply {
ZipUtils.unzipFile(zipFile, this)
}
val transferFile = unzipDir.resolve("${TransferData.TYPE}.json")
if (!transferFile.exists() || !transferFile.isFile) {
toast("导入无数据")
tempDir.deleteRecursively()
return
}
val data = withContext(Dispatchers.Default) {
json.decodeFromString(transferFile.readText())
}
val hasNewSubsItem = importTransferData(data)
val files = unzipDir.resolve(subsDirName)
if (files.exists()) {
val subscriptions = (files.listFiles { f -> f.isFile && f.name.endsWith(".json") }
?: emptyArray()).mapNotNull { f ->
try {
RawSubscription.parse(f.readText())
} catch (e: Exception) {
LogUtils.d(e)
null
}
}
subscriptions.forEach { subscription ->
if (LOCAL_SUBS_IDS.contains(subscription.id)) {
updateSubscription(subscription)
}
}
}
toast("导入成功")
tempDir.deleteRecursively()
if (hasNewSubsItem) {
delay(1000)
checkSubsUpdate(true)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/UserInfo.kt
================================================
package li.songe.gkd.data
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.Serializable
@Serializable
data class UserInfo(
val id: Int,
val name: String,
)
val otherUserMapFlow = MutableStateFlow(emptyMap())
================================================
FILE: app/src/main/kotlin/li/songe/gkd/data/Value.kt
================================================
package li.songe.gkd.data
data class Value( var value: T)
================================================
FILE: app/src/main/kotlin/li/songe/gkd/db/AppDb.kt
================================================
package li.songe.gkd.db
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.RenameColumn
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import li.songe.gkd.app
import li.songe.gkd.data.A11yEventLog
import li.songe.gkd.data.ActionLog
import li.songe.gkd.data.ActivityLog
import li.songe.gkd.data.AppConfig
import li.songe.gkd.data.AppVisitLog
import li.songe.gkd.data.CategoryConfig
import li.songe.gkd.data.Snapshot
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubsItem
import li.songe.gkd.util.dbFolder
import li.songe.gkd.util.json
@Database(
version = 14,
entities = [
SubsItem::class,
Snapshot::class,
SubsConfig::class,
CategoryConfig::class,
ActionLog::class,
ActivityLog::class,
AppConfig::class,
AppVisitLog::class,
A11yEventLog::class,
],
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7),
AutoMigration(from = 7, to = 8, spec = ActivityLog.ActivityLogV2Spec::class),
AutoMigration(from = 8, to = 9, spec = ActionLog.ActionLogSpec::class),
AutoMigration(from = 9, to = 10, spec = Migration9To10Spec::class),
AutoMigration(from = 10, to = 11, spec = Migration10To11Spec::class),
AutoMigration(from = 11, to = 12),
AutoMigration(from = 12, to = 13),
AutoMigration(from = 13, to = 14),
]
)
@TypeConverters(DbConverters::class)
abstract class AppDb : RoomDatabase() {
abstract fun subsItemDao(): SubsItem.SubsItemDao
abstract fun snapshotDao(): Snapshot.SnapshotDao
abstract fun subsConfigDao(): SubsConfig.SubsConfigDao
abstract fun appConfigDao(): AppConfig.AppConfigDao
abstract fun categoryConfigDao(): CategoryConfig.CategoryConfigDao
abstract fun actionLogDao(): ActionLog.ActionLogDao
abstract fun activityLogDao(): ActivityLog.ActivityLogDao
abstract fun appVisitLogDao(): AppVisitLog.AppLogDao
abstract fun a11yEventLogDao(): A11yEventLog.A11yEventLogDao
}
@RenameColumn(
tableName = "subs_config",
fromColumnName = "subs_item_id",
toColumnName = "subs_id"
)
@RenameColumn(
tableName = "category_config",
fromColumnName = "subs_item_id",
toColumnName = "subs_id"
)
class Migration9To10Spec : AutoMigrationSpec
@DeleteColumn(
tableName = "snapshot",
columnName = "app_name"
)
@DeleteColumn(
tableName = "snapshot",
columnName = "app_version_code"
)
@DeleteColumn(
tableName = "snapshot",
columnName = "app_version_name"
)
class Migration10To11Spec : AutoMigrationSpec
@Suppress("unused")
class DbConverters {
@TypeConverter
fun fromListStringToString(list: List): String {
return json.encodeToString(list)
}
@TypeConverter
fun fromStringToList(value: String): List {
if (value.isEmpty()) return emptyList()
return try {
json.decodeFromString(value)
} catch (_: Exception) {
emptyList()
}
}
}
object DbSet {
private val db by lazy {
Room.databaseBuilder(
app,
AppDb::class.java,
dbFolder.resolve("gkd.db").absolutePath
).fallbackToDestructiveMigration(false).build()
}
val subsItemDao get() = db.subsItemDao()
val subsConfigDao get() = db.subsConfigDao()
val snapshotDao get() = db.snapshotDao()
val actionLogDao get() = db.actionLogDao()
val categoryConfigDao get() = db.categoryConfigDao()
val activityLogDao get() = db.activityLogDao()
val appConfigDao get() = db.appConfigDao()
val appVisitLogDao get() = db.appVisitLogDao()
val a11yEventLogDao get() = db.a11yEventLogDao()
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/notif/Notif.kt
================================================
package li.songe.gkd.notif
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.core.net.toUri
import kotlinx.atomicfu.atomic
import li.songe.gkd.META
import li.songe.gkd.MainActivity
import li.songe.gkd.R
import li.songe.gkd.app
import li.songe.gkd.permission.foregroundServiceSpecialUseState
import li.songe.gkd.permission.notificationState
import li.songe.gkd.service.ActivityService
import li.songe.gkd.service.ButtonService
import li.songe.gkd.service.EventService
import li.songe.gkd.service.HttpService
import li.songe.gkd.service.ScreenshotService
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.componentName
import kotlin.reflect.KClass
// 相同的 request code 会导致后续 PendingIntent 失效
private val pendingIntentReqId = atomic(0)
data class Notif(
val channel: NotifChannel = NotifChannel.Default,
val id: Int,
val smallIcon: Int = R.drawable.ic_status,
val title: String,
val text: String? = null,
val ongoing: Boolean = true,
val autoCancel: Boolean = false,
val uri: String? = null,
val stopService: KClass? = null,
) {
private fun toNotification(): Notification {
val contextIntent = PendingIntent.getActivity(
app,
pendingIntentReqId.incrementAndGet(),
Intent().apply {
component = MainActivity::class.componentName
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
data = uri?.toUri()
},
PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(app, channel.id)
.setSmallIcon(smallIcon)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(contextIntent)
.setOngoing(ongoing)
.setAutoCancel(autoCancel)
if (stopService != null) {
val deleteIntent = PendingIntent.getBroadcast(
app,
pendingIntentReqId.incrementAndGet(),
StopServiceReceiver.getIntent(stopService),
PendingIntent.FLAG_IMMUTABLE
)
notification
.setDeleteIntent(deleteIntent)
.addAction(0, "停止", deleteIntent)
}
return notification.build()
}
fun notifySelf() {
if (!notificationState.updateAndGet()) return
if (!foregroundServiceSpecialUseState.updateAndGet()) return
@SuppressLint("MissingPermission")
NotificationManagerCompat.from(app).notify(id, toNotification())
}
context(service: Service)
fun notifyService() {
if (!notificationState.updateAndGet()) return
if (!foregroundServiceSpecialUseState.updateAndGet()) return
ServiceCompat.startForeground(
service,
id,
toNotification(),
if (AndroidTarget.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST else -1
)
}
}
val abNotif by lazy {
Notif(
id = 100,
title = META.appName,
text = "无障碍正在运行",
)
}
val screenshotNotif = Notif(
id = 101,
title = "截屏服务正在运行",
text = "保存快照时截取屏幕",
uri = "gkd://page/1",
stopService = ScreenshotService::class,
)
val buttonNotif = Notif(
id = 102,
title = "快照按钮服务正在运行",
text = "点击按钮捕获快照",
uri = "gkd://page/1",
stopService = ButtonService::class,
)
val httpNotif = Notif(
id = 103,
title = "HTTP服务正在运行",
uri = "gkd://page/1",
stopService = HttpService::class,
)
val exposeNotif = Notif(
id = 104,
title = "运行外部调用任务中",
text = "任务完成后自动关闭",
)
val snapshotNotif = Notif(
channel = NotifChannel.Snapshot,
id = 105,
title = "快照已保存",
ongoing = false,
autoCancel = true,
uri = "gkd://page/2",
)
val recordNotif = Notif(
id = 106,
title = "记录服务正在运行",
uri = "gkd://page/1",
stopService = ActivityService::class,
)
val eventNotif = Notif(
id = 107,
title = "事件服务正在运行",
uri = "gkd://page/1",
stopService = EventService::class,
)
================================================
FILE: app/src/main/kotlin/li/songe/gkd/notif/NotifChannel.kt
================================================
package li.songe.gkd.notif
import android.app.NotificationChannel
import android.app.NotificationManager
import androidx.core.app.NotificationManagerCompat
import li.songe.gkd.META
import li.songe.gkd.app
sealed class NotifChannel(
val id: String,
val name: String? = null,
val desc: String? = null,
) {
data object Default : NotifChannel(
id = "0",
)
data object Snapshot : NotifChannel(
id = "1",
name = "保存快照通知",
)
}
fun initChannel() {
val channels = arrayOf(NotifChannel.Default, NotifChannel.Snapshot)
val manager = NotificationManagerCompat.from(app)
// delete old channels
manager.notificationChannels.filter { channels.none { c -> c.id == it.id } }.forEach {
manager.deleteNotificationChannel(it.id)
}
// create/update new channels
channels.forEach {
val channel = NotificationChannel(
it.id,
it.name ?: META.appName,
NotificationManager.IMPORTANCE_LOW
).apply {
description = it.desc
}
manager.createNotificationChannel(channel)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/notif/StopServiceReceiver.kt
================================================
package li.songe.gkd.notif
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import li.songe.gkd.META
import li.songe.gkd.util.OnSimpleLife
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName
class StopServiceReceiver(private val service: Service) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
intent ?: return
if (intent.action == STOP_ACTION && intent.getStringExtra(STOP_ACTION) == service::class.jvmName) {
service.stopSelf()
}
}
companion object {
private val STOP_ACTION by lazy { META.appId + ".STOP_SERVICE" }
fun getIntent(clazz: KClass) = Intent().apply {
action = STOP_ACTION
putExtra(STOP_ACTION, clazz.jvmName)
setPackage(META.appId)
}
context(service: T)
fun autoRegister() where T : Service, T : OnSimpleLife {
val receiver = StopServiceReceiver(service)
service.onCreated {
ContextCompat.registerReceiver(
service,
receiver,
IntentFilter(STOP_ACTION),
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
service.onDestroyed {
service.unregisterReceiver(receiver)
}
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/permission/PermissionDialog.kt
================================================
package li.songe.gkd.permission
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.MainActivity
import li.songe.gkd.util.stopCoroutine
data class AuthReason(
val text: () -> String,
val confirm: ((Activity) -> Unit)? = null,
)
@Composable
fun AuthDialog(authReasonFlow: MutableStateFlow) {
val authAction = authReasonFlow.collectAsState().value
val context = LocalActivity.current as MainActivity
if (authAction != null) {
AlertDialog(
title = {
Text(text = "权限请求")
},
text = {
Text(text = authAction.text())
},
onDismissRequest = { authReasonFlow.value = null },
confirmButton = {
TextButton(onClick = {
authReasonFlow.value = null
authAction.confirm?.invoke(context)
}) {
Text(text = "确认")
}
},
dismissButton = {
TextButton(onClick = { authReasonFlow.value = null }) {
Text(text = "取消")
}
}
)
}
}
sealed class PermissionResult {
data object Granted : PermissionResult()
data class Denied(val doNotAskAgain: Boolean) : PermissionResult()
}
suspend fun requiredPermission(
context: MainActivity,
permissionState: PermissionState
) {
if (permissionState.updateAndGet()) return
val result = permissionState.request?.invoke(context)
if (result == null) {
context.mainVm.authReasonFlow.value = permissionState.reason
stopCoroutine()
} else if (result is PermissionResult.Denied) {
if (result.doNotAskAgain) {
context.mainVm.authReasonFlow.value = permissionState.reason
}
stopCoroutine()
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt
================================================
package li.songe.gkd.permission
import android.Manifest
import android.app.Activity
import android.app.AppOpsManager
import android.app.AppOpsManagerHidden
import android.content.pm.PackageManager
import android.provider.Settings
import com.hjq.permissions.XXPermissions
import com.hjq.permissions.permission.PermissionLists
import com.hjq.permissions.permission.base.IPermission
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.updateAndGet
import li.songe.gkd.MainActivity
import li.songe.gkd.MainViewModel
import li.songe.gkd.app
import li.songe.gkd.appScope
import li.songe.gkd.shizuku.SafeAppOpsService
import li.songe.gkd.shizuku.SafePackageManager
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.ui.AppOpsAllowRoute
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateAllAppInfo
import li.songe.gkd.util.updateAppMutex
import rikka.shizuku.Shizuku
class PermissionState(
val name: String,
val check: () -> Boolean,
val request: (suspend (context: MainActivity) -> PermissionResult)? = null,
/**
* show it when user doNotAskAgain
*/
val reason: AuthReason? = null,
) {
val stateFlow = MutableStateFlow(false)
val value get() = stateFlow.value
fun updateAndGet(): Boolean {
return stateFlow.updateAndGet { check() }
}
fun updateChanged(): Boolean {
return value != updateAndGet()
}
fun checkOrToast(): Boolean = if (!updateAndGet()) {
val r = updateAndGet()
if (!r) {
reason?.text?.let { toast(it()) }
}
r
} else {
true
}
}
private suspend fun asyncRequestPermission(
context: Activity,
permission: IPermission,
): PermissionResult {
if (XXPermissions.isGrantedPermission(context, permission)) {
return PermissionResult.Granted
}
val deferred = CompletableDeferred()
XXPermissions.with(context)
.unchecked()
.permission(permission)
.request { grantedList, _ ->
if (grantedList.contains(permission)) {
PermissionResult.Granted
} else {
PermissionResult.Denied(
XXPermissions.isDoNotAskAgainPermissions(
context,
arrayOf(permission)
)
)
}.let { deferred.complete(it) }
}
return deferred.await()
}
private fun checkAllowedOp(op: String): Boolean = app.appOpsManager.checkOpNoThrow(
op,
android.os.Process.myUid(),
app.packageName
).let {
it != AppOpsManager.MODE_IGNORED && it != AppOpsManager.MODE_ERRORED
}
// https://github.com/gkd-kit/gkd/issues/954
// https://github.com/gkd-kit/gkd/issues/887
val foregroundServiceSpecialUseState by lazy {
PermissionState(
name = "特殊用途的前台服务",
check = {
if (AndroidTarget.UPSIDE_DOWN_CAKE) {
checkAllowedOp(AppOpsManagerHidden.OPSTR_FOREGROUND_SERVICE_SPECIAL_USE)
} else {
true
}
},
reason = AuthReason(
text = { "当前操作权限「特殊用途的前台服务」已被限制, 请先解除限制" },
confirm = {
MainViewModel.instance.navigatePage(AppOpsAllowRoute)
},
),
)
}
// https://github.com/orgs/gkd-kit/discussions/1234
val accessA11yState by lazy {
PermissionState(
name = "访问无障碍",
check = {
if (AndroidTarget.Q) {
checkAllowedOp(AppOpsManagerHidden.OPSTR_ACCESS_ACCESSIBILITY)
} else {
true
}
},
)
}
val createA11yOverlayState by lazy {
PermissionState(
name = "创建无障碍悬浮窗",
check = {
if (SafeAppOpsService.supportCreateA11yOverlay) {
checkAllowedOp(AppOpsManagerHidden.OPSTR_CREATE_ACCESSIBILITY_OVERLAY)
} else {
true
}
},
)
}
const val Manifest_permission_GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS"
val getAppOpsStatsState by lazy {
PermissionState(
name = "获取应用权限状态",
check = {
app.checkGrantedPermission(Manifest_permission_GET_APP_OPS_STATS)
},
)
}
private var canRestrictsRead = true
val accessRestrictedSettingsState by lazy {
PermissionState(
name = "访问受限设置",
check = {
if (canRestrictsRead && AndroidTarget.UPSIDE_DOWN_CAKE && getAppOpsStatsState.updateAndGet()) {
try {
// https://cs.android.com/android/platform/superproject/+/android-14.0.0_r55:frameworks/base/services/core/java/com/android/server/appop/AppOpsService.java;l=4237
checkAllowedOp(AppOpsManagerHidden.OPSTR_ACCESS_RESTRICTED_SETTINGS)
} catch (_: SecurityException) {
// https://cs.android.com/android/platform/superproject/+/android-14.0.0_r54:frameworks/base/services/core/java/com/android/server/appop/AppOpsService.java;l=4227
canRestrictsRead = false
true
}
} else {
true
}
},
)
}
val appOpsRestrictStateList by lazy {
arrayOf(
accessA11yState,
createA11yOverlayState,
accessRestrictedSettingsState,
foregroundServiceSpecialUseState,
)
}
val appOpsRestrictedFlow by lazy {
combine(
*appOpsRestrictStateList.map { it.stateFlow }.toTypedArray(),
) { list ->
list.any { !it }
}.stateIn(appScope, SharingStarted.Eagerly, false)
}
val notificationState by lazy {
val permission = PermissionLists.getNotificationServicePermission()
PermissionState(
name = "通知权限",
check = {
XXPermissions.isGrantedPermission(app, permission)
},
request = { asyncRequestPermission(it, permission) },
reason = AuthReason(
text = { "当前操作需要「通知权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(app, permission)
}
),
)
}
val canQueryPkgState by lazy {
val permission = PermissionLists.getGetInstalledAppsPermission()
val supported by lazy { permission.isSupportRequestPermission(app) }
PermissionState(
name = "读取应用列表权限",
check = {
if (supported) {
// 此框架内部有两个 printStackTrace 导致每次检测都会打印日志污染控制台
XXPermissions.isGrantedPermission(app, permission)
} else {
true
}
},
request = {
asyncRequestPermission(it, permission)
},
reason = AuthReason(
text = { "当前操作需要「读取应用列表权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(app, permission)
}
),
)
}
val canDrawOverlaysState by lazy {
PermissionState(
name = "悬浮窗权限",
check = {
// https://developer.android.com/security/fraud-prevention/activities?hl=zh-cn#hide_overlay_windows
Settings.canDrawOverlays(app)
},
reason = AuthReason(
text = {
"当前操作需要「悬浮窗权限」\n请先前往权限页面授权"
},
confirm = {
XXPermissions.startPermissionActivity(
app,
PermissionLists.getSystemAlertWindowPermission()
)
}
),
)
}
val canWriteExternalStorage by lazy {
PermissionState(
name = "写入外部存储权限",
check = {
if (AndroidTarget.Q) {
true
} else {
app.checkGrantedPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
},
request = {
if (AndroidTarget.Q) {
PermissionResult.Granted
} else {
asyncRequestPermission(it, PermissionLists.getWriteExternalStoragePermission())
}
},
reason = AuthReason(
text = { "当前操作需要「写入外部存储权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(
app,
PermissionLists.getWriteExternalStoragePermission()
)
}
),
)
}
val ignoreBatteryOptimizationsState by lazy {
val permission = PermissionLists.getRequestIgnoreBatteryOptimizationsPermission()
PermissionState(
name = "忽略电池优化权限",
check = {
app.powerManager.isIgnoringBatteryOptimizations(app.packageName)
},
request = {
asyncRequestPermission(it, permission)
},
reason = AuthReason(
text = { "当前操作需要「忽略电池优化权限」\n请先前往权限页面授权" },
confirm = {
XXPermissions.startPermissionActivity(
app,
permission
)
}
),
)
}
val writeSecureSettingsState by lazy {
PermissionState(
name = "写入安全设置权限",
check = { app.checkGrantedPermission(Manifest.permission.WRITE_SECURE_SETTINGS) },
)
}
private fun shizukuCheckGranted(): Boolean {
if (Shizuku.getBinder()?.isBinderAlive != true) return false
val granted = try {
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
} catch (_: Throwable) {
false
}
if (!granted) return false
val u = shizukuContextFlow.value.packageManager ?: SafePackageManager.newBinder()
return u?.isSafeMode != null
}
val shizukuGrantedState by lazy {
PermissionState(
name = "Shizuku 权限",
check = { shizukuCheckGranted() },
)
}
val allPermissionStates by lazy {
listOf(
notificationState,
foregroundServiceSpecialUseState,
accessA11yState,
createA11yOverlayState,
getAppOpsStatsState,
accessRestrictedSettingsState,
canDrawOverlaysState,
canWriteExternalStorage,
ignoreBatteryOptimizationsState,
writeSecureSettingsState,
canQueryPkgState,
shizukuGrantedState,
)
}
fun updatePermissionState() {
allPermissionStates.forEach {
if (it === canQueryPkgState && !updateAppMutex.mutex.isLocked) {
if (canQueryPkgState.updateChanged()) {
updateAllAppInfo()
}
} else {
it.updateAndGet()
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/A11yService.kt
================================================
package li.songe.gkd.service
import android.accessibilityservice.AccessibilityService
import android.annotation.SuppressLint
import android.content.Context.WINDOW_SERVICE
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.view.Display
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import com.google.android.accessibility.selecttospeak.SelectToSpeakService
import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.a11y.A11yCommonImpl
import li.songe.gkd.a11y.A11yRuleEngine
import li.songe.gkd.a11y.topActivityFlow
import li.songe.gkd.a11y.updateTopActivity
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.store.updateEnableAutomator
import li.songe.gkd.util.AndroidTarget
import li.songe.gkd.util.AutomatorModeOption
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.OnA11yLife
import li.songe.gkd.util.componentName
import li.songe.gkd.util.runMainPost
import li.songe.gkd.util.toast
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@SuppressLint("AccessibilityPolicy")
open class A11yService : AccessibilityService(), OnA11yLife, A11yCommonImpl {
override val mode get() = AutomatorModeOption.A11yMode
override val scope = useScope()
override val windowNodeInfo: AccessibilityNodeInfo? get() = rootInActiveWindow
override val windowInfos: List get() = windows
override suspend fun screenshot(): Bitmap? = suspendCoroutine { continuation ->
if (AndroidTarget.R) {
takeScreenshot(
Display.DEFAULT_DISPLAY,
application.mainExecutor,
object : TakeScreenshotCallback {
override fun onFailure(errorCode: Int) = continuation.resume(null)
override fun onSuccess(screenshot: ScreenshotResult) = try {
continuation.resume(
Bitmap.wrapHardwareBuffer(
screenshot.hardwareBuffer, screenshot.colorSpace
)
)
} finally {
screenshot.hardwareBuffer.close()
}
}
)
} else {
continuation.resume(null)
}
}
override val ruleEngine by lazy { A11yRuleEngine(this) }
override fun onCreate() = onCreated()
override fun onServiceConnected() = onA11yConnected()
override fun onInterrupt() {}
override fun onDestroy() = onDestroyed()
override fun onAccessibilityEvent(event: AccessibilityEvent?) = ruleEngine.onA11yEvent(event)
val startTime = System.currentTimeMillis()
override var justStarted: Boolean = true
get() {
if (field) {
field = System.currentTimeMillis() - startTime < 3_000
}
return field
}
private var tempShutdownFlag = false
override fun shutdown(temp: Boolean) {
if (temp) {
tempShutdownFlag = true
}
disableSelf()
}
private var destroyed = false
private var connected = false
val wm by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }
init {
useLogLifecycle()
useAliveFlow(isRunning)
onA11yConnected { instance = this }
onDestroyed { instance = null }
onCreated {
if (currentAppUseA11y) {
updateEnableAutomator(true)
} else {
toast("当前为自动化模式,无障碍将自动关闭", forced = true)
runMainPost(1) { shutdown(true) }
}
}
onDestroyed {
if (tempShutdownFlag) {
toast("无障碍局部关闭")
} else {
toast("无障碍已关闭")
updateEnableAutomator(false)
}
}
useAliveOverlayView()
onCreated { StatusService.autoStart() }
onDestroyed {
shizukuContextFlow.value.topCpn()?.let { cpn ->
// com.android.systemui
if (!topActivityFlow.value.sameAs(cpn.packageName, cpn.className)) {
updateTopActivity(cpn.packageName, cpn.className)
}
}
}
onDestroyed { destroyed = true }
onA11yConnected {
connected = true
toast("无障碍已启动")
if (currentAppUseA11y) {
ruleEngine.onA11yConnected()
}
}
onCreated {
runMainPost(3000) {
if (!(destroyed || connected)) {
toast("无障碍启动超时,请尝试关闭重启", forced = true)
}
}
}
}
companion object {
val a11yCn by lazy { SelectToSpeakService::class.componentName }
val isRunning = MutableStateFlow(false)
@Volatile
var instance: A11yService? = null
private set
}
}
private fun A11yService.useAliveOverlayView() {
val context = this
var aliveView: View? = null
val wm by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }
fun removeA11View() {
if (aliveView != null) {
wm.removeView(aliveView)
aliveView = null
}
}
fun addA11View() {
removeA11View()
val tempView = View(context)
val lp = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
format = PixelFormat.TRANSLUCENT
flags =
flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
gravity = Gravity.START or Gravity.TOP
width = 1
height = 1
packageName = context.packageName
}
try {
// 某些设备 android.view.WindowManager$BadTokenException
wm.addView(tempView, lp)
aliveView = tempView
} catch (e: Throwable) {
aliveView = null
LogUtils.d(e)
toast("添加无障碍保活失败\n请尝试重启无障碍")
}
}
onA11yConnected { addA11View() }
onDestroyed { removeA11View() }
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/ActivityService.kt
================================================
package li.songe.gkd.service
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import li.songe.gkd.a11y.ActivityScene
import li.songe.gkd.a11y.topActivityFlow
import li.songe.gkd.a11y.updateTopActivity
import li.songe.gkd.notif.StopServiceReceiver
import li.songe.gkd.notif.recordNotif
import li.songe.gkd.permission.canDrawOverlaysState
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.ui.component.PerfIcon
import li.songe.gkd.ui.style.iconTextSize
import li.songe.gkd.util.copyText
import li.songe.gkd.util.startForegroundServiceByClass
import li.songe.gkd.util.stopServiceByClass
class ActivityService : OverlayWindowService(
positionKey = "activity"
) {
val activityOkFlow by lazy {
combine(A11yService.isRunning, shizukuContextFlow) { a, b ->
a || b.ok
}.stateIn(scope = lifecycleScope, started = SharingStarted.Eagerly, initialValue = false)
}
@Composable
override fun ComposeContent() {
val bgColor = MaterialTheme.colorScheme.surface
Column(
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.background(bgColor.copy(alpha = 0.9f))
.width(IntrinsicSize.Max)
.padding(4.dp)
) {
CompositionLocalProvider(LocalContentColor provides contentColorFor(bgColor)) {
val topActivity by topActivityFlow.collectAsState()
val hasAuth by activityOkFlow.collectAsState()
ClosableTitle(
title = if (hasAuth) "记录服务" else "记录服务(无权限)"
)
if (hasAuth) {
Box {
Column(
modifier = Modifier.padding(start = 4.dp)
) {
RowText(text = topActivity.appId)
RowText(
text = topActivity.shortActivityId,
color = MaterialTheme.colorScheme.secondary
)
}
if (topActivity.number > 0) {
Text(
text = topActivity.number.toString(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.align(Alignment.TopEnd)
.zIndex(1f)
.clip(MaterialTheme.shapes.extraSmall)
.padding(end = 4.dp),
)
}
}
}
}
}
}
init {
useLogLifecycle()
useAliveFlow(isRunning)
useAliveToast("记录服务")
StopServiceReceiver.autoRegister()
onCreated { recordNotif.notifyService() }
onCreated {
lifecycleScope.launch {
topActivityFlow.collect {
recordNotif.copy(text = it.format()).notifyService()
}
}
if (!A11yService.isRunning.value) {
shizukuContextFlow.value.topCpn()?.let { cpn ->
updateTopActivity(
appId = cpn.packageName,
activityId = cpn.className,
scene = ActivityScene.TaskStack,
)
}
}
}
}
companion object {
val isRunning = MutableStateFlow(false)
fun start() {
if (!canDrawOverlaysState.checkOrToast()) return
startForegroundServiceByClass(ActivityService::class)
}
fun stop() = stopServiceByClass(ActivityService::class)
}
}
@Composable
private fun RowText(text: String?, color: Color = Color.Unspecified) {
Row {
Text(text = text ?: "null", color = color, modifier = Modifier.weight(1f, false))
if (text != null) {
Spacer(modifier = Modifier.width(4.dp))
PerfIcon(
imageVector = PerfIcon.ContentCopy,
modifier = Modifier
.clip(MaterialTheme.shapes.extraSmall)
.clickable(onClick = {
copyText(text)
})
.iconTextSize(),
)
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/ActivityTileService.kt
================================================
package li.songe.gkd.service
class ActivityTileService : BaseTileService() {
override val activeFlow = ActivityService.isRunning
init {
onTileClicked {
if (ActivityService.isRunning.value) {
ActivityService.stop()
} else {
ActivityService.start()
}
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/BaseTileService.kt
================================================
package li.songe.gkd.service
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import li.songe.gkd.util.OnTileLife
abstract class BaseTileService : TileService(), OnTileLife {
override fun onCreate() = onCreated()
override fun onStartListening() = onStartListened()
override fun onClick() = onTileClicked()
override fun onStopListening() = onStopListened()
override fun onDestroy() = onDestroyed()
abstract val activeFlow: StateFlow
override val scope = useScope()
val listeningFlow = MutableStateFlow(false).apply {
onStartListened { value = true }
onStopListened { value = false }
}
init {
onStartListened {
val t = System.currentTimeMillis()
if (t - lastA11yFixTime > 3_000L) {
lastA11yFixTime = t
fixRestartAutomatorService()
}
}
onTileClicked { StatusService.autoStart() }
scope.launch {
combine(
activeFlow,
listeningFlow
) { v1, v2 -> v1 to v2 }.collect { (active, listening) ->
if (listening) {
qsTile.state = if (active) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
qsTile.updateTile()
}
}
}
}
}
private var lastA11yFixTime = 0L
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/ButtonService.kt
================================================
package li.songe.gkd.service
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.MutableStateFlow
import li.songe.gkd.appScope
import li.songe.gkd.notif.StopServiceReceiver
import li.songe.gkd.notif.buttonNotif
import li.songe.gkd.permission.canDrawOverlaysState
import li.songe.gkd.ui.component.PerfIcon
import li.songe.gkd.util.SnapshotExt
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.startForegroundServiceByClass
import li.songe.gkd.util.stopServiceByClass
class ButtonService : OverlayWindowService(
positionKey = "button"
) {
override fun onClickView() = appScope.launchTry {
SnapshotExt.captureSnapshot()
}.let { }
override fun onLongClickView() = stopSelf()
@Composable
override fun ComposeContent() {
val alpha = 0.75f
PerfIcon(
imageVector = PerfIcon.CenterFocusWeak,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = alpha))
.size(40.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = alpha),
)
}
init {
useAliveFlow(isRunning)
useAliveToast("快照按钮服务")
onCreated { buttonNotif.notifyService() }
StopServiceReceiver.autoRegister()
}
companion object {
val isRunning = MutableStateFlow(false)
fun start() {
if (!canDrawOverlaysState.checkOrToast()) return
startForegroundServiceByClass(ButtonService::class)
}
fun stop() = stopServiceByClass(ButtonService::class)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/ButtonTileService.kt
================================================
package li.songe.gkd.service
class ButtonTileService : BaseTileService() {
override val activeFlow = ButtonService.isRunning
init {
onTileClicked {
if (ButtonService.isRunning.value) {
ButtonService.stop()
} else {
ButtonService.start()
}
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/EventService.kt
================================================
package li.songe.gkd.service
import android.view.accessibility.AccessibilityEvent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import li.songe.gkd.META
import li.songe.gkd.appScope
import li.songe.gkd.data.A11yEventLog
import li.songe.gkd.data.toA11yEventLog
import li.songe.gkd.db.DbSet
import li.songe.gkd.notif.StopServiceReceiver
import li.songe.gkd.notif.eventNotif
import li.songe.gkd.permission.canDrawOverlaysState
import li.songe.gkd.ui.EventLogCard
import li.songe.gkd.ui.component.LocalNumberCharWidth
import li.songe.gkd.ui.component.PerfIcon
import li.songe.gkd.ui.component.PerfIconButton
import li.songe.gkd.ui.component.isAtBottom
import li.songe.gkd.ui.component.measureNumberTextWidth
import li.songe.gkd.ui.share.ListPlaceholder
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.startForegroundServiceByClass
import li.songe.gkd.util.stopServiceByClass
class EventService : OverlayWindowService(positionKey = "event") {
val eventLogs = mutableStateListOf()
private var tempEventId = 0
private var firstToBottom = false
@Composable
override fun ComposeContent() {
val bgColor = MaterialTheme.colorScheme.surface
CompositionLocalProvider(
LocalContentColor provides contentColorFor(bgColor),
) {
val listState = key(eventLogs.isEmpty()) { rememberLazyListState() }
val isAtBottom by listState.isAtBottom()
val subScope = rememberCoroutineScope()
SideEffect {
val latestId = eventLogs.lastOrNull()?.id ?: 0
if (tempEventId != latestId) {
tempEventId = latestId
if (isAtBottom) {
subScope.launch { listState.scrollToItem(eventLogs.lastIndex) }
}
}
}
Column(
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.background(bgColor.copy(alpha = 0.9f))
.width(256.dp)
.padding(4.dp)
) {
ClosableTitle(
title = if (A11yService.isRunning.collectAsState().value) "事件服务" else "事件服务(无权限)"
)
val textStyle = MaterialTheme.typography.labelSmall
val numCharWidth = measureNumberTextWidth(textStyle)
CompositionLocalProvider(
LocalTextStyle provides textStyle,
LocalNumberCharWidth provides numCharWidth,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(eventLogs, { it.id }) {
EventLogCard(
eventLog = it,
modifier = Modifier.padding(horizontal = 2.dp)
)
}
item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {
Spacer(modifier = Modifier.height(2.dp))
}
}
if (eventLogs.isNotEmpty() && !isAtBottom) {
if (!firstToBottom) {
firstToBottom = true
SideEffect {
subScope.launch { listState.scrollToItem(eventLogs.lastIndex) }
}
}
var count by remember { mutableIntStateOf(-1) }
LaunchedEffect(eventLogs.last().id) { count++ }
Column(
modifier = Modifier
.align(Alignment.BottomEnd)
.width(IntrinsicSize.Min),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (count > 0) {
Text(text = "+$count")
}
PerfIconButton(
imageVector = PerfIcon.ArrowDownward,
onClick = {
subScope.launch {
listState.scrollToItem(eventLogs.lastIndex)
}
},
)
}
}
}
}
}
}
}
val tempEventListFlow = MutableStateFlow(emptyList()).apply {
appScope.launch {
while (scope.isActive) {
delay(1000)
val list = getAndUpdate { emptyList() }
if (list.isNotEmpty()) {
DbSet.a11yEventLogDao.insert(list)
}
}
}
}
init {
logAutoId = 0
instance = this
onDestroyed {
instance = null
logAutoId = 0
}
scope.launch {
logAutoId = (DbSet.a11yEventLogDao.maxId() ?: 0).coerceAtLeast(1)
}
useLogLifecycle()
useAliveFlow(isRunning)
useAliveToast("事件服务")
StopServiceReceiver.autoRegister()
onCreated { eventNotif.notifyService() }
}
companion object {
private var instance: EventService? = null
private var logAutoId = 0
fun logEvent(event: AccessibilityEvent) {
val service = instance ?: return
if (event.packageName == META.appId) return
if (logAutoId == 0) return
logAutoId++
val eventLog = event.toA11yEventLog(logAutoId)
service.eventLogs.add(eventLog)
service.tempEventListFlow.update { it + eventLog }
if (service.eventLogs.size >= 256) {
service.eventLogs.removeRange(0, 64)
}
if (eventLog.id % 100 == 0) {
appScope.launchTry { DbSet.a11yEventLogDao.deleteKeepLatest() }
}
}
val isRunning = MutableStateFlow(false)
fun start() {
if (!canDrawOverlaysState.checkOrToast()) return
startForegroundServiceByClass(EventService::class)
}
fun stop() = stopServiceByClass(EventService::class)
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/EventTileService.kt
================================================
package li.songe.gkd.service
class EventTileService : BaseTileService() {
override val activeFlow = EventService.isRunning
init {
onTileClicked {
if (EventService.isRunning.value) {
EventService.stop()
} else {
EventService.start()
}
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/ExposeService.kt
================================================
package li.songe.gkd.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import li.songe.gkd.app
import li.songe.gkd.appScope
import li.songe.gkd.notif.exposeNotif
import li.songe.gkd.syncFixState
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.SnapshotExt
import li.songe.gkd.util.componentName
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.runMainPost
import li.songe.gkd.util.shFolder
import li.songe.gkd.util.toast
class ExposeService : Service() {
override fun onBind(intent: Intent?): Binder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
appScope.launchTry {
try {
handleIntent(intent)
} finally {
runMainPost(1000) { stopSelf() }
}
}
return super.onStartCommand(intent, flags, startId)
}
suspend fun handleIntent(intent: Intent?) {
val expose = intent?.getIntExtra("expose", 0) ?: 0
val data = intent?.getStringExtra("data")
LogUtils.d("ExposeService::handleIntent", expose, data)
when (expose) {
-1 -> StatusService.autoStart()
0 -> SnapshotExt.captureSnapshot()
1 -> {
toast("执行成功", forced = true)
syncFixState()
}
else -> {
toast("未知调用: expose=$expose data=$data", forced = true)
}
}
}
override fun onCreate() {
super.onCreate()
exposeNotif.notifyService()
}
companion object {
fun initCommandFile() {
val commandText = template
.replace("{service}", ExposeService::class.componentName.flattenToShortString())
shFolder.resolve("expose.sh").writeText(commandText)
}
fun exposeIntent(expose: Int, data: String? = null): Intent {
return Intent(app, ExposeService::class.java).apply {
putExtra("expose", expose)
if (data != null) {
putExtra("data", data)
}
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
}
}
private const val template = $$"""set -euo pipefail
echo '> start expose.sh'
p=''
if [ -n "${1:-}" ]; then
p+=" --ei expose $1"
fi
if [ -n "${2:-}" ]; then
p+=" --es data $2"
fi
am start-foreground-service -n {service} $p
echo '> expose.sh end'
"""
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt
================================================
package li.songe.gkd.service
import android.provider.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import li.songe.gkd.META
import li.songe.gkd.a11y.systemRecentCn
import li.songe.gkd.a11y.topActivityFlow
import li.songe.gkd.accessRestrictedSettingsShowFlow
import li.songe.gkd.app
import li.songe.gkd.appScope
import li.songe.gkd.isActivityVisible
import li.songe.gkd.permission.writeSecureSettingsState
import li.songe.gkd.shizuku.AutomationService
import li.songe.gkd.shizuku.shizukuContextFlow
import li.songe.gkd.shizuku.uiAutomationFlow
import li.songe.gkd.store.actualA11yScopeAppList
import li.songe.gkd.store.actualBlockA11yAppList
import li.songe.gkd.store.storeFlow
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.mapState
import li.songe.gkd.util.runMainPost
import li.songe.gkd.util.toast
class GkdTileService : BaseTileService() {
override val activeFlow = combine(A11yService.isRunning, uiAutomationFlow) { a11y, automator ->
a11y || automator != null
}.stateIn(scope, SharingStarted.Eagerly, false)
init {
onTileClicked { switchAutomatorService() }
}
}
private val modifyA11yMutex = Mutex()
private const val A11Y_AWAIT_START_TIME = 2000L
private const val A11Y_AWAIT_FIX_TIME = 1000L
private fun modifyA11yRun(block: suspend () -> Unit) {
if (modifyA11yMutex.isLocked) return
appScope.launchTry(Dispatchers.IO) {
if (modifyA11yMutex.isLocked) return@launchTry
modifyA11yMutex.withLock { block() }
}
}
private suspend fun switchA11yService() {
if (A11yService.isRunning.value) {
A11yService.instance?.disableSelf()
} else {
if (!writeSecureSettingsState.updateAndGet()) {
if (!writeSecureSettingsState.value) {
toast("请先授予「写入安全设置权限」")
return
}
}
val names = app.getSecureA11yServices()
app.putSecureInt(Settings.Secure.ACCESSIBILITY_ENABLED, 1)
if (names.contains(A11yService.a11yCn)) { // 当前无障碍异常, 重启服务
names.remove(A11yService.a11yCn)
app.putSecureA11yServices(names)
delay(A11Y_AWAIT_FIX_TIME)
}
names.add(A11yService.a11yCn)
app.putSecureA11yServices(names)
delay(A11Y_AWAIT_START_TIME)
// https://github.com/orgs/gkd-kit/discussions/799
if (!A11yService.isRunning.value) {
toast("开启无障碍失败")
accessRestrictedSettingsShowFlow.value = true
return
}
}
}
private fun switchAutomationService() {
val newEnabled = uiAutomationFlow.value == null
uiAutomationFlow.value?.shutdown()
if (newEnabled && shizukuContextFlow.value.ok) {
AutomationService.tryConnect()
}
}
fun switchAutomatorService() = modifyA11yRun {
if (currentAppUseA11y) {
switchA11yService()
} else {
switchAutomationService()
}
}
private fun skipBlockApp(): Boolean {
if (storeFlow.value.enableBlockA11yAppList) {
val topAppId = if (isActivityVisible || app.justStarted) {
META.appId
} else {
shizukuContextFlow.value.topCpn()?.packageName
}
if (topAppId != null && topAppId in actualBlockA11yAppList) {
return true
}
}
return false
}
private suspend fun fixA11yService() {
if (!A11yService.isRunning.value && writeSecureSettingsState.updateAndGet()) {
if (skipBlockApp()) return
val names = app.getSecureA11yServices()
val a11yBroken = names.contains(A11yService.a11yCn)
if (a11yBroken) {
// 无障碍出现故障, 重启服务
names.remove(A11yService.a11yCn)
app.putSecureA11yServices(names)
// 必须等待一段时间, 否则概率不会触发系统重启无障碍
delay(A11Y_AWAIT_FIX_TIME)
if (!currentAppUseA11y) return
}
names.add(A11yService.a11yCn)
app.putSecureA11yServices(names)
delay(A11Y_AWAIT_START_TIME)
if (currentAppUseA11y && !A11yService.isRunning.value) {
toast("重启无障碍失败")
accessRestrictedSettingsShowFlow.value = true
}
}
}
private fun fixAutomationService() {
if (uiAutomationFlow.value == null && shizukuContextFlow.value.ok) {
if (skipBlockApp()) return
if (currentAppUseA11y) return
AutomationService.tryConnect(true)
}
}
fun fixRestartAutomatorService() = modifyA11yRun {
if (storeFlow.value.enableAutomator) {
if (currentAppUseA11y) {
fixA11yService()
} else {
fixAutomationService()
}
}
}
val currentAppUseA11y
get() = storeFlow.value.useA11y || topAppIdFlow.value in actualA11yScopeAppList
val currentAppBlocked
get() = storeFlow.value.enableBlockA11yAppList && topAppIdFlow.value in actualBlockA11yAppList
private fun innerForcedUpdateA11yService(disabled: Boolean) {
if (!storeFlow.value.enableAutomator) {
return
}
if (disabled) {
A11yService.instance?.shutdown(true)
uiAutomationFlow.value?.shutdown(true)
return
}
if (currentAppUseA11y) {
if (A11yService.isRunning.value) {
return
}
if (!writeSecureSettingsState.stateFlow.value) {
return
}
val names = app.getSecureA11yServices()
names.add(A11yService.a11yCn)
app.putSecureA11yServices(names)
} else {
AutomationService.tryConnect(true)
}
}
private fun forcedUpdateA11yService(disabled: Boolean) = modifyA11yRun {
innerForcedUpdateA11yService(disabled)
}
const val A11Y_WHITE_APP_AWAIT_TIME = 3000L
@Volatile
private var lastAppIdChangeTime = 0L
val topAppIdFlow = MutableStateFlow("")
val a11yPartDisabledFlow by lazy {
topAppIdFlow.mapState(appScope) {
actualBlockA11yAppList.contains(it)
}
}
fun updateTopTaskAppId(value: String) {
if (storeFlow.value.enableBlockA11yAppList || actualA11yScopeAppList.isNotEmpty()) {
topAppIdFlow.value = value
}
}
fun initA11yWhiteAppList() {
val actualFlow = topAppIdFlow.drop(1)
appScope.launch(Dispatchers.Main) {
actualFlow.collect {
lastAppIdChangeTime = System.currentTimeMillis()
if (!currentAppBlocked) {
if (topActivityFlow.value.sameAs(systemRecentCn) && currentAppUseA11y) {
// 切换无障碍会造成卡顿,在最近任务界面时,延迟这个卡顿
val tempTime = lastAppIdChangeTime
runMainPost(A11Y_WHITE_APP_AWAIT_TIME) {
if (tempTime == lastAppIdChangeTime) {
forcedUpdateA11yService(false)
}
}
} else {
// 切换自动化不会卡顿,直接启动
forcedUpdateA11yService(false)
}
}
}
}
appScope.launch(Dispatchers.Main) {
actualFlow.debounce(A11Y_WHITE_APP_AWAIT_TIME).collect {
if (currentAppBlocked) {
forcedUpdateA11yService(true)
}
}
}
}
================================================
FILE: app/src/main/kotlin/li/songe/gkd/service/HttpService.kt
================================================
package li.songe.gkd.service
import android.app.Service
import android.content.Intent
import android.util.Log
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.hooks.CallFailed
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.origin
import io.ktor.server.request.httpMethod
import io.ktor.server.request.receive
import io.ktor.server.request.receiveText
import io.ktor.server.request.uri
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondFile
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import li.songe.gkd.a11y.A11yRuleEngine
import li.songe.gkd.appScope
import li.songe.gkd.data.AppInfo
import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.data.GkdAction
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.selfAppInfo
import li.songe.gkd.db.DbSet
import li.songe.gkd.notif.StopServiceReceiver
import li.songe.gkd.notif.httpNotif
import li.songe.gkd.store.storeFlow
import li.songe.gkd.util.LOCAL_HTTP_SUBS_ID
import li.songe.gkd.util.LogUtils
import li.songe.gkd.util.OnSimpleLife
import li.songe.gkd.util.SERVER_SCRIPT_URL
import li.songe.gkd.util.SnapshotExt
import li.songe.gkd.util.SnapshotExt.getMinSnapshot
import li.songe.gkd.util.deleteSubscription
import li.songe.gkd.util.getIpAddressInLocalNetwork
import li.songe.gkd.util.isPortAvailable
import li.songe.gkd.util.keepNullJson
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.mapState
import li.songe.gkd.util.startForegroundServiceByClass
import li.songe.gkd.util.stopServiceByClass
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubscription
class HttpService : Service(), OnSimpleLife {
override fun onBind(intent: Intent?) = null
override fun onCreate() {
super.onCreate()
onCreated()
}
override fun onDestroy() {
super.onDestroy()
onDestroyed()
}
override val scope = useScope()
val httpServerPortFlow = storeFlow.mapState(scope) { s -> s.httpServerPort }
init {
useLogLifecycle()
useAliveFlow(isRunning)
useAliveToast("HTTP服务")
StopServiceReceiver.autoRegister()
onCreated {
scope.launchTry(Dispatchers.IO) {
httpServerPortFlow.collect {
localNetworkIpsFlow.value = getIpAddressInLocalNetwork()
}
}
}
onDestroyed {
if (storeFlow.value.autoClearMemorySubs) {
deleteSubscription(LOCAL_HTTP_SUBS_ID)
}
httpServerFlow.value = null
}
onCreated {
httpNotif.notifyService()
scope.launchTry(Dispatchers.IO) {
httpServerPortFlow.collect { port ->
val isReboot = httpServerFlow.value != null
httpServerFlow.apply {
value?.stop()
value = null
}
if (!isPortAvailable(port)) {
toast("端口 $port 被占用,请更换后重试")
stopSelf()
return@collect
}
httpServerFlow.value = try {
scope.createServer(port).apply { start() }
} catch (e: Exception) {
toast("HTTP服务启动失败:${e.stackTraceToString()}")
LogUtils.d("HTTP服务启动失败", e)
null
}
if (httpServerFlow.value == null) {
stopSelf()
} else if (isReboot) {
toast("HTTP服务重启成功")
}
}
}
}
}
companion object {
val httpServerFlow = MutableStateFlow