commit 94738a2b412e6030a4da45775adf1c483ffc8125 Spenser Truex <truex@equwal.com> 2026-09-21 22:47:06 -0700 Remove irc recover pages (#2) * remove the IRC bouncer offer The IRC bouncer does not run any more. No process listens on port 6969. Remove the Stripe payment link from the contact page, and delete the two posts that offer the service. * recover the La Cervido page The Wayback Machine has one capture of equwal.com/cervido, from 2025-02-18. The page is story 16 of Budhismaj Rakontoj, and the budhismaj-rakontoj page links to it. Its audio files were not captured, so MISSING-ASSETS.md lists them. * add sacc to the git mirror equwal/sacc is a new mirror of git://bitreich.org/sacc. Regenerate git/ with stagit-gen.py so that the mirror lists sacc. The run also adds the new public repo ideamine and refreshes pages for repos that changed. Line endings stay LF and file modes stay 644, as in the earlier mirror. * Regenerate the git mirror with the newest repos and commits Run stagit-gen.py against the equwal GitHub account. The mirror gains 18 repos pushed since the last run: assistkey, awesome-rebind, deploy-handoff, equwal, ink-dim, ink-recents, ink-update, rebind, sbm-android, sbm-extension, sbm-suckless, sbm-sync, subplz-web, subread-android, subread-dictionary, subread-extension, subread-overlay, and subread.koplugin. Existing repos with new commits since the last run get updated log, file, and commit pages.
.../4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html | 82 +- .../75492a1107063b545c771844e77c34a852554350.html | 96 +- .../c9441131467ba02e70e8cc466bfd2b769d09d985.html | 80 +- .../cecddfa6396707ab94ada6098f3668145df39511.html | 148 +- .../f3d174752dda70690cf073701c79901755cfc192.html | 72 +- .../00e3cd71a896d0cacbf1a0416961a80538cd015f.html | 1817 ++ .../09ac2ef42c0cfe4ac02cb643b0fc4f27fc112264.html | 186 + .../1030d2b2c355341a737cc75a6d72dc054e70456f.html | 248 + .../14ef6461af9264c077df23e292d93f8e6e0dd3ec.html | 215 + .../189bad36a4deca54fb94fdddba9c7f460c53f396.html | 155 + .../18aa22e4b41c083822c33956426c6df6037c7d22.html | 33 + .../1ab4a2cc65f6d6e8eb775b37d2b51f5b1ef566b2.html | 119 + .../1b52b05fd8797783930b1e94d817dc7f63abdda8.html | 254 + .../1c4d7451a4fd224d4bda724c61060039b14d1ed2.html | 1666 ++ .../1d10d271c06db0aca5534a1e153611d8041fe3ed.html | 247 + .../1d617cdedebbfc8047d37df2e912df5f7b120887.html | 3496 ++++ .../209a44c25ae9ccd34a7ea5a86a3e8f1b97f1972c.html | 52 + .../30fa1216cf865a558deafdd18a2525934ced9e70.html | 385 + .../328b1248fba2a5aabc0071101d2803ac9fc686be.html | 180 + .../3f9281b01a14288df49c5bde6a25ee694c3e150c.html | 860 + .../4327f281b85f9643e53187551cd8b1ebe00e95bd.html | 217 + .../4a491fc2fc7a8e98776a04a5a43e75e7c0bb559b.html | 203 + .../4a87b58b1392385687a9cc5ad4b766d5e9aa2fec.html | 1208 ++ .../4ca570c824b8e012203b8bae0eb0fee2479d829c.html | 139 + .../4d0fbdf2a413720fdcc68c9c74079c98b52cc213.html | 303 + .../4de4826a7e15c5f7fb14cd763c7a2aa951596e3e.html | 466 + .../4ee7a575fb8d12ea6d25b82f08f4a723d7db365c.html | 1129 ++ .../54edd309f4bf40127ca0c968a656c2b567e748bd.html | 940 + .../5ff0c95e5e678b5eb3e639443da57dc84ef0536d.html | 226 + .../6ffddab52b082f1395514ef2eda60ea55986d949.html | 1305 ++ .../79cd2e6f42ff751d775fc7430ad5e6118df03f98.html | 412 + .../7becf58ad8536b26a8cd4ee8e2f3abe4d9cd5afb.html | 406 + .../89ba7f3d3b8126cd2c8d729a674b9b6df85a2708.html | 589 + .../8cf0165f18ece43dfdf0ce6da9042478c54fc9e8.html | 3098 ++++ .../9d5b6e5ea7c989f9537b567773c7e2871b2c2c2c.html | 237 + .../a45553746c352033b7f7c80e9134266b7f513d33.html | 223 + .../a4f50eb15068384a7ee0df89da5a38e11e1279cd.html | 188 + .../b175979d4b009fba543fa5fd47428c20a38e88d4.html | 1837 ++ .../c85a2296cf5f388a7ee502f68547f65f2626edf8.html | 4368 +++++ .../c94dfb74a7fa682296961c328de25acceea3bd9a.html | 1657 ++ .../e1b50b40b548990726c7cd2fcab98e45cbf9c8b3.html | 60 + .../ed07fa386b703539875fcd429819879f64b1e670.html | 992 ++ .../ed479e346759565223f46c617d0d31230b2c4515.html | 1193 ++ .../f89a23751e04f42320285d32c1d13de4d2fc34b6.html | 264 + .../fb8bf902379868901245845e16e27c1a01a36c99.html | 58 + .../ff747da6b3f57ce17162c210ecab0a5e0711335d.html | 129 + git/assistkey/file/.gitattributes.html | 19 + .../file/.github/workflows/tag-check.yml.html | 46 + git/assistkey/file/.gitignore.html | 36 + git/assistkey/file/CHANGELOG.md.html | 413 + git/assistkey/file/LICENSE.html | 684 + git/assistkey/file/README.md.html | 486 + git/assistkey/file/app/build.gradle.kts.html | 182 + .../equwal/assistkey/license/PlayBilling.kt.html | 314 + .../file/app/src/debug/AndroidManifest.xml.html | 29 + .../assistkey/debug/FakeRecognitionService.kt.html | 44 + .../equwal/assistkey/debug/InsertReceiver.kt.html | 30 + .../dev/equwal/assistkey/debug/ShotApp.kt.html | 62 + .../file/app/src/fdroid/AndroidManifest.xml.html | 30 + .../dev/equwal/assistkey/bundle/Bundled.kt.html | 73 + .../equwal/assistkey/license/PlayBilling.kt.html | 40 + .../java/dev/equwal/assistkey/license/Tip.kt.html | 16 + .../file/app/src/full/AndroidManifest.xml.html | 38 + .../app/src/full/assets/bundled/NOTICE.txt.html | 51 + .../dev/equwal/assistkey/bundle/Bundled.kt.html | 117 + .../java/dev/equwal/assistkey/license/Tip.kt.html | 16 + .../java/dev/equwal/assistkey/shell/Shell.kt.html | 241 + .../file/app/src/main/AndroidManifest.xml.html | 255 + .../equwal/assistkey/channel/AiKeyActivity.kt.html | 114 + .../assistkey/channel/AssistActivity.kt.html | 29 + .../assistkey/channel/CameraShimActivity.kt.html | 28 + .../dev/equwal/assistkey/channel/Channel.kt.html | 85 + .../equwal/assistkey/channel/ChannelEntry.kt.html | 94 + .../dev/equwal/assistkey/channel/Channels.kt.html | 194 + .../assistkey/channel/WalletActivity.kt.html | 26 + .../equwal/assistkey/channel/WalletService.kt.html | 36 + .../dev/equwal/assistkey/device/Detect.kt.html | 353 + .../dev/equwal/assistkey/device/Device.kt.html | 151 + .../dev/equwal/assistkey/display/ExtraDim.kt.html | 138 + .../equwal/assistkey/engine/GestureEngine.kt.html | 204 + .../assistkey/engine/KeyFilterService.kt.html | 439 + .../dev/equwal/assistkey/engine/KeyLog.kt.html | 85 + .../equwal/assistkey/engine/ScreenButton.kt.html | 130 + .../equwal/assistkey/home/RecentsActivity.kt.html | 42 + .../dev/equwal/assistkey/license/License.kt.html | 205 + .../java/dev/equwal/assistkey/license/Sku.kt.html | 30 + .../java/dev/equwal/assistkey/menu/Menu.kt.html | 86 + .../dev/equwal/assistkey/menu/MenuActivity.kt.html | 113 + .../equwal/assistkey/menu/MenuEditActivity.kt.html | 98 + .../dev/equwal/assistkey/model/ActionSpec.kt.html | 116 + .../assistkey/model/ComponentPayload.kt.html | 40 + .../equwal/assistkey/model/GlobalAction.kt.html | 45 + .../java/dev/equwal/assistkey/model/HwKey.kt.html | 66 + .../dev/equwal/assistkey/model/Presets.kt.html | 46 + .../dev/equwal/assistkey/model/Trigger.kt.html | 88 + .../dev/equwal/assistkey/native/NavNative.kt.html | 109 + .../equwal/assistkey/native/PowerNative.kt.html | 175 + .../equwal/assistkey/native/ViwoodsBridge.kt.html | 132 + .../equwal/assistkey/route/ActionRouter.kt.html | 210 + .../equwal/assistkey/route/ServiceHolder.kt.html | 25 + .../dev/equwal/assistkey/setup/DeviceView.kt.html | 132 + .../assistkey/setup/GuidedSetupActivity.kt.html | 418 + .../java/dev/equwal/assistkey/setup/Route.kt.html | 136 + .../equwal/assistkey/shell/PowerControl.kt.html | 146 + .../dev/equwal/assistkey/store/AutoBackup.kt.html | 108 + .../dev/equwal/assistkey/store/Bindings.kt.html | 84 + .../equwal/assistkey/store/SettingsFile.kt.html | 125 + .../java/dev/equwal/assistkey/store/Store.kt.html | 88 + .../assistkey/ui/AccessibilityDisclosure.kt.html | 52 + .../assistkey/ui/ActionPickerActivity.kt.html | 259 + .../equwal/assistkey/ui/AdvancedActivity.kt.html | 105 + .../dev/equwal/assistkey/ui/BackupActivity.kt.html | 190 + .../dev/equwal/assistkey/ui/BundledRows.kt.html | 38 + .../dev/equwal/assistkey/ui/ChordActivity.kt.html | 80 + .../dev/equwal/assistkey/ui/DetectActivity.kt.html | 102 + .../equwal/assistkey/ui/DisplayActivity.kt.html | 83 + .../dev/equwal/assistkey/ui/HacksActivity.kt.html | 81 + .../equwal/assistkey/ui/HomeHubActivity.kt.html | 41 + .../equwal/assistkey/ui/KeyTesterActivity.kt.html | 91 + .../dev/equwal/assistkey/ui/KeysActivity.kt.html | 88 + .../equwal/assistkey/ui/LicenseActivity.kt.html | 200 + .../dev/equwal/assistkey/ui/MainActivity.kt.html | 205 + .../equwal/assistkey/ui/NavigationActivity.kt.html | 268 + .../assistkey/ui/PermissionsActivity.kt.html | 237 + .../dev/equwal/assistkey/ui/PowerActivity.kt.html | 228 + .../dev/equwal/assistkey/ui/ReportActivity.kt.html | 154 + .../dev/equwal/assistkey/ui/SetupActivity.kt.html | 92 + .../dev/equwal/assistkey/ui/ShellActivity.kt.html | 106 + .../java/dev/equwal/assistkey/ui/Summary.kt.html | 92 + .../dev/equwal/assistkey/ui/TimingActivity.kt.html | 93 + .../assistkey/ui/TriggerListActivity.kt.html | 113 + .../main/java/dev/equwal/assistkey/ui/Ui.kt.html | 560 + .../equwal/assistkey/ui/ViwoodsActivity.kt.html | 72 + .../dev/equwal/assistkey/ui/VoiceActivity.kt.html | 96 + .../dev/equwal/assistkey/voice/Dictation.kt.html | 173 + .../assistkey/voice/DictationActivity.kt.html | 201 + .../dev/equwal/assistkey/voice/TextInsert.kt.html | 69 + .../res/drawable/ic_launcher_foreground.xml.html | 30 + .../res/mipmap-anydpi-v26/ic_launcher.xml.html | 16 + .../mipmap-anydpi-v26/ic_launcher_round.xml.html | 16 + .../file/app/src/main/res/values/colors.xml.html | 14 + .../file/app/src/main/res/values/strings.xml.html | 19 + .../file/app/src/main/res/values/styles.xml.html | 39 + .../res/xml/accessibility_service_config.xml.html | 27 + .../src/main/res/xml/quick_access_wallet.xml.html | 21 + .../dev/equwal/assistkey/bundle/Bundled.kt.html | 73 + .../java/dev/equwal/assistkey/license/Tip.kt.html | 19 + .../java/dev/equwal/assistkey/shell/Shell.kt.html | 61 + .../dev/equwal/assistkey/device/DetectTest.kt.html | 176 + .../equwal/assistkey/device/DeviceKeysTest.kt.html | 49 + .../assistkey/device/SystemRecentsTest.kt.html | 35 + .../assistkey/display/ExtraDimStepTest.kt.html | 67 + .../assistkey/license/LicenseDecideTest.kt.html | 71 + .../dev/equwal/assistkey/menu/MenuTest.kt.html | 43 + .../assistkey/model/ActionSpecNameTest.kt.html | 40 + .../assistkey/model/ComponentPayloadTest.kt.html | 47 + .../dev/equwal/assistkey/setup/RouteTest.kt.html | 171 + .../assistkey/store/SettingsFileTest.kt.html | 124 + .../assistkey/store/StoredFormatTest.kt.html | 95 + .../dev/equwal/assistkey/ui/SummaryTest.kt.html | 96 + .../assistkey/voice/DictationPickTest.kt.html | 47 + .../assistkey/voice/DictationRouteTest.kt.html | 36 + git/assistkey/file/build.gradle.kts.html | 15 + git/assistkey/file/docs/MONETIZATION.md.html | 95 + .../metadata/android/en-US/changelogs/17.txt.html | 11 + .../android/en-US/full_description.txt.html | 34 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + git/assistkey/file/fdroid/README.md.html | 43 + .../file/fdroid/dev.equwal.assistkey.yml.html | 40 + git/assistkey/file/gradle.properties.html | 25 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/assistkey/file/gradlew.bat.html | 92 + git/assistkey/file/gradlew.html | 258 + git/assistkey/file/play/CHECKLIST.md.html | 178 + git/assistkey/file/play/DECLARATIONS.md.html | 113 + git/assistkey/file/play/LISTING.md.html | 133 + git/assistkey/file/play/PRIVACY.md.html | 104 + git/assistkey/file/play/graphics/Render.java.html | 90 + git/assistkey/file/settings.gradle.kts.html | 34 + git/assistkey/file/tools/ai-key.html | 31 + git/assistkey/file/tools/e2e/ai-key-return.sh.html | 46 + git/assistkey/file/tools/e2e/power-wake.sh.html | 59 + git/assistkey/file/tools/nav-mode.html | 56 + git/assistkey/file/tools/nav-mode.ps1.html | 37 + git/assistkey/files.html | 11 + git/assistkey/index.html | 11 + git/assistkey/refs.html | 11 + .../81fd1566352ec6b0f90d08b7355225af88798d05.html | 44 + .../b47cecb77a62e2f77e791e42771448de56f8f76e.html | 103 + .../e7bb04998c9cfc978adeee69dd3aebc9178c6627.html | 44 + git/awesome-rebind/file/README.md.html | 99 + git/awesome-rebind/files.html | 11 + git/awesome-rebind/index.html | 11 + git/awesome-rebind/refs.html | 11 + .../43d1af77a2b9ae0b9330a1efe90ba4a1abdc2f65.html | 1236 ++ .../file/.claude-plugin/marketplace.json.html | 32 + .../file/.claude-plugin/plugin.json.html | 23 + git/deploy-handoff/file/.gitattributes.html | 11 + git/deploy-handoff/file/.gitignore.html | 15 + git/deploy-handoff/file/LICENSE.html | 31 + git/deploy-handoff/file/README.md.html | 88 + git/deploy-handoff/file/pyproject.toml.html | 28 + .../file/skills/deploy-handoff/SKILL.md.html | 129 + .../file/skills/deploy-handoff/config.toml.html | 34 + .../file/skills/deploy-handoff/handoff.py.html | 379 + .../skills/deploy-handoff/test_handoff.py.html | 470 + git/deploy-handoff/files.html | 11 + git/deploy-handoff/index.html | 11 + git/deploy-handoff/refs.html | 11 + .../a0ce9d8553eadd410aeee52b0915724c2d472400.html | 69 + git/equwal/file/README.md.html | 56 + git/equwal/files.html | 11 + git/equwal/index.html | 11 + git/equwal/refs.html | 11 + .../01b032b66a03fcd3a69b94ff66a39315e7322493.html | 111 + .../131d20531da89d73f667eba61c47a3f672ed731e.html | 1538 ++ .../2f026cce0259c72ccc730161c4a17f36bc339c43.html | 1068 ++ .../71ab31e7a64250413289c0c472dd5e81f1a4ba49.html | 2758 +++ .../78466858965dee37edb88c9d6acfa6d1f8c08d90.html | 2765 +++ .../a82ce66038bb90f1720333eee03a88db83e84de8.html | 1202 ++ .../af3a5361733fc9c5901a9139ce2ba72917c5a95e.html | 668 + .../file/.claude-plugin/marketplace.json.html | 2 +- git/ideamine/file/.claude-plugin/plugin.json.html | 2 +- git/ideamine/file/README.md.html | 366 +- git/ideamine/file/bin/ideamine.js.html | 475 +- git/ideamine/file/dashboard/index.html.html | 1531 ++ git/ideamine/file/package-lock.json.html | 72 + git/ideamine/file/package.json.html | 72 +- git/ideamine/file/skills/idea-go/SKILL.md.html | 24 - git/ideamine/file/skills/idea-triage/SKILL.md.html | 22 - git/ideamine/file/skills/ideas-all/SKILL.md.html | 25 + git/ideamine/file/skills/ideas-cat/SKILL.md.html | 19 + git/ideamine/file/skills/ideas-done/SKILL.md.html | 21 + git/ideamine/file/skills/ideas-find/SKILL.md.html | 21 + git/ideamine/file/skills/ideas-go/SKILL.md.html | 28 + .../file/skills/ideas-groups/SKILL.md.html | 21 + git/ideamine/file/skills/ideas-ls/SKILL.md.html | 21 + .../file/skills/ideas-reopen/SKILL.md.html | 21 + git/ideamine/file/skills/ideas-rm/SKILL.md.html | 19 + git/ideamine/file/skills/ideas-sort/SKILL.md.html | 18 + git/ideamine/file/skills/ideas-sync/SKILL.md.html | 18 + git/ideamine/file/skills/ideas-watch/SKILL.md.html | 18 + git/ideamine/file/skills/ideas-web/SKILL.md.html | 18 + git/ideamine/file/skills/ideas/SKILL.md.html | 10 +- git/ideamine/file/src/archive.js.html | 72 + git/ideamine/file/src/claude.js.html | 362 +- git/ideamine/file/src/config.js.html | 124 + git/ideamine/file/src/embed.js.html | 328 + git/ideamine/file/src/hook.js.html | 299 +- git/ideamine/file/src/mcp.js.html | 698 +- git/ideamine/file/src/projects.js.html | 93 + git/ideamine/file/src/publish.js.html | 249 + git/ideamine/file/src/render.js.html | 174 +- git/ideamine/file/src/rubric.js.html | 116 +- git/ideamine/file/src/serve.js.html | 547 + git/ideamine/file/src/store.js.html | 714 +- git/ideamine/file/src/sync.js.html | 366 + git/ideamine/file/src/watch.js.html | 169 + git/ideamine/file/tests/cli.test.js.html | 53 + git/ideamine/file/tests/embed.test.js.html | 173 + .../file/tests/fixtures/fake-claude.js.html | 82 +- .../file/tests/fixtures/fake-memstate.js.html | 49 + .../file/tests/fixtures/fake-server.js.html | 61 + .../file/tests/fixtures/server-process.js.html | 62 + git/ideamine/file/tests/hook.test.js.html | 290 +- git/ideamine/file/tests/mcp.test.js.html | 209 +- git/ideamine/file/tests/projects.test.js.html | 66 + git/ideamine/file/tests/publish.test.js.html | 151 + git/ideamine/file/tests/serve.test.js.html | 368 + git/ideamine/file/tests/store.test.js.html | 167 +- git/ideamine/file/tests/sync.test.js.html | 243 + git/ideamine/file/tests/triage.test.js.html | 156 +- git/ideamine/file/tests/watch.test.js.html | 107 + git/ideamine/files.html | 2 +- git/ideamine/index.html | 2 +- git/ideamine/refs.html | 2 +- git/index.html | 2 +- .../8f531c86e1ccb3f93e00a476ddd15aca88daea28.html | 31 + .../e95f3cb5dcede91ca204c322be87d34b24e298a9.html | 2838 +++ git/ink-dim/file/.gitattributes.html | 19 + git/ink-dim/file/.gitignore.html | 33 + git/ink-dim/file/CHANGELOG.md.html | 23 + git/ink-dim/file/LICENSE.html | 684 + git/ink-dim/file/PRIVACY.md.html | 33 + git/ink-dim/file/README.md.html | 106 + git/ink-dim/file/app/build.gradle.kts.html | 88 + .../file/app/src/debug/AndroidManifest.xml.html | 19 + .../java/dev/equwal/inkdim/debug/ShotApp.kt.html | 64 + .../file/app/src/main/AndroidManifest.xml.html | 77 + .../src/main/java/dev/equwal/inkdim/Dim.kt.html | 98 + .../java/dev/equwal/inkdim/DimTileService.kt.html | 40 + .../dev/equwal/inkdim/SettingsActivity.kt.html | 120 + .../src/main/java/dev/equwal/inkdim/Shell.kt.html | 132 + .../src/main/java/dev/equwal/inkdim/Toggle.kt.html | 63 + .../java/dev/equwal/inkdim/ToggleActivity.kt.html | 83 + .../app/src/main/java/dev/equwal/inkdim/Ui.kt.html | 344 + .../res/drawable/ic_launcher_foreground.xml.html | 34 + .../res/drawable/ic_shortcut_settings.xml.html | 28 + .../app/src/main/res/drawable/ic_tile.xml.html | 34 + .../res/mipmap-anydpi-v26/ic_launcher.xml.html | 16 + .../mipmap-anydpi-v26/ic_launcher_round.xml.html | 16 + .../file/app/src/main/res/values/colors.xml.html | 14 + .../file/app/src/main/res/values/strings.xml.html | 16 + .../file/app/src/main/res/values/styles.xml.html | 40 + .../file/app/src/main/res/xml/shortcuts.xml.html | 28 + .../test/java/dev/equwal/inkdim/DimTest.kt.html | 113 + git/ink-dim/file/build.gradle.kts.html | 15 + .../metadata/android/en-US/changelogs/1.txt.html | 16 + .../android/en-US/full_description.txt.html | 38 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + git/ink-dim/file/fdroid/README.md.html | 18 + git/ink-dim/file/fdroid/dev.equwal.inkdim.yml.html | 37 + git/ink-dim/file/gradle.properties.html | 16 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/ink-dim/file/gradlew.bat.html | 92 + git/ink-dim/file/gradlew.html | 258 + git/ink-dim/file/settings.gradle.kts.html | 34 + git/ink-dim/files.html | 11 + git/ink-dim/index.html | 11 + git/ink-dim/refs.html | 11 + .../8b05f81823ce4775d76c29549c3c3d604f3e9127.html | 114 + .../9eff80311bc6fcf25ebecf46e61276b3f1671fb5.html | 31 + .../ab2e8ea23d1b8b1cf1b698697ad1741d269f28a9.html | 136 + .../c08aa1d0bce23410530afc12600273e51a21090a.html | 30 + .../ca171b932a0a67c936a8af2a5895cca8aad9c59e.html | 2923 ++++ git/ink-recents/file/.gitattributes.html | 19 + git/ink-recents/file/.gitignore.html | 33 + git/ink-recents/file/CHANGELOG.md.html | 30 + git/ink-recents/file/LICENSE.html | 684 + git/ink-recents/file/PRIVACY.md.html | 39 + git/ink-recents/file/README.md.html | 106 + git/ink-recents/file/app/build.gradle.kts.html | 89 + .../file/app/src/debug/AndroidManifest.xml.html | 19 + .../dev/equwal/inkrecents/debug/ShotApp.kt.html | 63 + .../file/app/src/main/AndroidManifest.xml.html | 92 + .../main/java/dev/equwal/inkrecents/Apps.kt.html | 167 + .../dev/equwal/inkrecents/RecentsActivity.kt.html | 404 + .../equwal/inkrecents/RecentsTileService.kt.html | 45 + .../dev/equwal/inkrecents/SettingsActivity.kt.html | 106 + .../main/java/dev/equwal/inkrecents/Shell.kt.html | 143 + .../src/main/java/dev/equwal/inkrecents/Ui.kt.html | 337 + .../res/drawable/ic_launcher_foreground.xml.html | 28 + .../app/src/main/res/drawable/ic_tile.xml.html | 26 + .../res/mipmap-anydpi-v26/ic_launcher.xml.html | 16 + .../mipmap-anydpi-v26/ic_launcher_round.xml.html | 16 + .../file/app/src/main/res/values/colors.xml.html | 14 + .../file/app/src/main/res/values/strings.xml.html | 16 + .../file/app/src/main/res/values/styles.xml.html | 26 + git/ink-recents/file/build.gradle.kts.html | 15 + .../metadata/android/en-US/changelogs/1.txt.html | 16 + .../metadata/android/en-US/changelogs/2.txt.html | 11 + .../android/en-US/full_description.txt.html | 42 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + git/ink-recents/file/fdroid/README.md.html | 18 + .../file/fdroid/dev.equwal.inkrecents.yml.html | 37 + git/ink-recents/file/gradle.properties.html | 16 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/ink-recents/file/gradlew.bat.html | 92 + git/ink-recents/file/gradlew.html | 258 + git/ink-recents/file/settings.gradle.kts.html | 34 + git/ink-recents/files.html | 11 + git/ink-recents/index.html | 11 + git/ink-recents/refs.html | 11 + .../9cce63f2c36d0eea09134d7b7a27f88f68a908f5.html | 30 + .../a37f8c7977ea38acd934bf57fe7afda120f3d593.html | 3768 ++++ git/ink-update/file/.gitattributes.html | 19 + git/ink-update/file/.gitignore.html | 33 + git/ink-update/file/CHANGELOG.md.html | 23 + git/ink-update/file/LICENSE.html | 684 + git/ink-update/file/PRIVACY.md.html | 56 + git/ink-update/file/README.md.html | 122 + git/ink-update/file/app/build.gradle.kts.html | 87 + .../file/app/src/debug/AndroidManifest.xml.html | 19 + .../dev/equwal/inkupdate/debug/ShotApp.kt.html | 64 + .../file/app/src/main/AndroidManifest.xml.html | 70 + .../java/dev/equwal/inkupdate/CheckJob.kt.html | 72 + .../main/java/dev/equwal/inkupdate/Checker.kt.html | 143 + .../main/java/dev/equwal/inkupdate/Decide.kt.html | 211 + .../java/dev/equwal/inkupdate/MainActivity.kt.html | 207 + .../src/main/java/dev/equwal/inkupdate/Net.kt.html | 83 + .../main/java/dev/equwal/inkupdate/Notify.kt.html | 84 + .../main/java/dev/equwal/inkupdate/Store.kt.html | 131 + .../src/main/java/dev/equwal/inkupdate/Ui.kt.html | 353 + .../java/dev/equwal/inkupdate/Versions.kt.html | 108 + .../main/java/dev/equwal/inkupdate/Watched.kt.html | 79 + .../res/drawable/ic_launcher_foreground.xml.html | 29 + .../app/src/main/res/drawable/ic_notify.xml.html | 30 + .../res/mipmap-anydpi-v26/ic_launcher.xml.html | 16 + .../mipmap-anydpi-v26/ic_launcher_round.xml.html | 16 + .../file/app/src/main/res/values/colors.xml.html | 14 + .../file/app/src/main/res/values/strings.xml.html | 42 + .../file/app/src/main/res/values/styles.xml.html | 26 + .../java/dev/equwal/inkupdate/DecideTest.kt.html | 169 + .../java/dev/equwal/inkupdate/JsonTest.kt.html | 177 + .../java/dev/equwal/inkupdate/VersionTest.kt.html | 134 + git/ink-update/file/build.gradle.kts.html | 15 + .../metadata/android/en-US/changelogs/1.txt.html | 16 + .../android/en-US/full_description.txt.html | 38 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + git/ink-update/file/fdroid/README.md.html | 18 + .../file/fdroid/dev.equwal.inkupdate.yml.html | 37 + git/ink-update/file/gradle.properties.html | 16 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/ink-update/file/gradlew.bat.html | 92 + git/ink-update/file/gradlew.html | 258 + git/ink-update/file/settings.gradle.kts.html | 34 + git/ink-update/files.html | 11 + git/ink-update/index.html | 11 + git/ink-update/refs.html | 11 + .../1566c4a665139cd9c4952964446851f405f283ef.html | 60 +- .../41ddcff927a4be87b77dc315b41f4e57f4c8f29b.html | 41 + .../6bae059f6039f76bbf0d887c0df56393005e0b59.html | 49 + .../6eef08b6e4060a1cb90d00b4494b71441ff71997.html | 93 + .../86cdd6d5b4580f0b64a0c99a8873131182f4fd90.html | 244 + .../a46098eb049582d57db32b74eef28e152f9bc1d5.html | 101 + .../a5b9a97486ef08387e1e7ae9e33c72f9cbe5aae3.html | 86 + .../d3a492480165c0c48ab1ae5bb10b855630feaeed.html | 60 + .../d84b6fb4f2a8d2fe890bc948bc54640fbcfc4edd.html | 40 + .../ed64355a287c59fb499fb2027fedad05edc7aca2.html | 34 + .../f66536956507006badcdd22bd048a5ef923b26ea.html | 71 + git/rebind/file/CHANGELOG.md.html | 202 + git/rebind/file/PRIVACY.md.html | 104 + git/rebind/file/README.md.html | 107 + git/rebind/files.html | 11 + git/rebind/index.html | 11 + git/rebind/refs.html | 11 + .../78452c690c319e2b4dc0db58b0047cf40a1f2af7.html | 1481 ++ .../930d56644577f94bc9781087139c5aa15dce7514.html | 3616 ++++ git/recentlywritten/file/MISSING-ASSETS.md.html | 2204 ++- git/recentlywritten/file/build.sh.html | 358 +- ...0f92f5666de0e698ac6739e5f6aea0e3ceccf.html.html | 2 +- ...92a1107063b545c771844e77c34a852554350.html.html | 2 +- ...41131467ba02e70e8cc466bfd2b769d09d985.html.html | 2 +- ...ddfa6396707ab94ada6098f3668145df39511.html.html | 2 +- ...174752dda70690cf073701c79901755cfc192.html.html | 2 +- git/recentlywritten/files.html | 2 +- git/recentlywritten/index.html | 2 +- git/recentlywritten/refs.html | 2 +- .../373b6a927fb7ba831144b89e7ce180a5ea918bb4.html | 2960 ++++ .../d17b3c104b12c0327550085ea0dea903341e7981.html | 34 + git/sbm-android/file/.gitattributes.html | 14 + git/sbm-android/file/.github/FUNDING.yml.html | 11 + git/sbm-android/file/.gitignore.html | 16 + git/sbm-android/file/LICENSE.html | 672 + git/sbm-android/file/README.md.html | 68 + git/sbm-android/file/app/build.gradle.kts.html | 90 + .../file/app/src/main/AndroidManifest.xml.html | 50 + .../main/java/com/equwal/sbm/AddActivity.kt.html | 77 + .../app/src/main/java/com/equwal/sbm/Fuzzy.kt.html | 57 + .../main/java/com/equwal/sbm/MainActivity.kt.html | 276 + .../app/src/main/java/com/equwal/sbm/Store.kt.html | 79 + .../app/src/main/java/com/equwal/sbm/Sync.kt.html | 164 + .../app/src/main/java/com/equwal/sbm/Tsv.kt.html | 97 + .../res/drawable/ic_launcher_foreground.xml.html | 21 + .../app/src/main/res/layout/activity_add.xml.html | 76 + .../app/src/main/res/layout/activity_main.xml.html | 56 + .../src/main/res/layout/dialog_sign_in.xml.html | 59 + .../file/app/src/main/res/layout/item.xml.html | 35 + .../file/app/src/main/res/menu/main.xml.html | 42 + .../main/res/mipmap-anydpi/ic_launcher.xml.html | 16 + .../app/src/main/res/values-night/themes.xml.html | 24 + .../file/app/src/main/res/values/strings.xml.html | 63 + .../file/app/src/main/res/values/themes.xml.html | 26 + .../app/src/main/res/xml/backup_rules.xml.html | 19 + .../main/res/xml/data_extraction_rules.xml.html | 29 + .../src/test/java/com/equwal/sbm/FuzzyTest.kt.html | 69 + .../src/test/java/com/equwal/sbm/SyncTest.kt.html | 26 + .../src/test/java/com/equwal/sbm/TsvTest.kt.html | 134 + git/sbm-android/file/build.gradle.kts.html | 16 + .../metadata/android/en-US/changelogs/1.txt.html | 11 + .../android/en-US/full_description.txt.html | 27 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + .../file/fdroid/com.equwal.sbm.yml.html | 36 + git/sbm-android/file/gradle.properties.html | 12 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/sbm-android/file/gradlew.bat.html | 104 + git/sbm-android/file/gradlew.html | 261 + git/sbm-android/file/play/listing.md.html | 70 + git/sbm-android/file/play/make-graphics.ps1.html | 52 + git/sbm-android/file/settings.gradle.kts.html | 28 + git/sbm-android/files.html | 11 + git/sbm-android/index.html | 11 + git/sbm-android/refs.html | 11 + .../0eedfdafda7fdc9649365d4f558d1ae43079223a.html | 31 + .../9cb786cc7cc09704beed8e6c58e6a527d882e163.html | 88 + .../e4b672fc5dddbae5246875d531ab0762a8cd00b9.html | 153 + .../f01781a4bdc450169611f4d48d2d0f52833e6ceb.html | 1867 ++ git/sbm-extension/file/.gitattributes.html | 12 + git/sbm-extension/file/.github/FUNDING.yml.html | 11 + git/sbm-extension/file/.gitignore.html | 12 + git/sbm-extension/file/LICENSE.html | 672 + git/sbm-extension/file/README.md.html | 68 + git/sbm-extension/file/background.js.html | 164 + git/sbm-extension/file/icons/make-icons.ps1.html | 47 + git/sbm-extension/file/lib/fuzzy.js.html | 67 + git/sbm-extension/file/lib/tsv.js.html | 103 + git/sbm-extension/file/manifest.json.html | 86 + git/sbm-extension/file/options.html.html | 54 + git/sbm-extension/file/options.js.html | 110 + git/sbm-extension/file/package-lock.json.html | 64 + git/sbm-extension/file/package.json.html | 24 + git/sbm-extension/file/popup.html.html | 40 + git/sbm-extension/file/popup.js.html | 141 + git/sbm-extension/file/store/listing.md.html | 79 + git/sbm-extension/file/style.css.html | 32 + git/sbm-extension/file/test/fuzzy.test.js.html | 68 + git/sbm-extension/file/test/tsv.test.js.html | 86 + git/sbm-extension/file/web-ext-config.mjs.html | 23 + git/sbm-extension/files.html | 11 + git/sbm-extension/index.html | 11 + git/sbm-extension/refs.html | 11 + .../17c35931744210ec4d428681f1a0a6791a5d6bae.html | 262 + .../20dcf6104b65502609ddd5de50e595764c30bbc6.html | 126 + .../35e13666096ac3d48031222e663c868c835f1209.html | 34 + .../48da2ae40401b3e1c91cc044f03f1fcb2eb46856.html | 141 + .../4ec93f33bfb00c01530c4aeafcf7cf8d3efe5ab9.html | 54 + .../527ba89a9c50cf8ed41030628858f8e08d1fca0b.html | 20 + .../54eff69143e540758de47ff5762dd89a8f391a8d.html | 134 + .../62d340ecf4fc4e01a4fbe229f182681683d318c8.html | 574 + .../73a2cfb70e9595ff30364c46be96b8cfbcd90a9e.html | 51 + .../805d1e939219754e77114aeb9bddb75144e941b3.html | 34 + .../84d1f87d0807097581665554d770a6bf7f437662.html | 32 + .../85303225927cb04a2d20ab64374cda69f3a71aa6.html | 20 + .../a2a5d76a0cc524589a272390055c2ebc1e5800bd.html | 71 + .../ab1adc5c042505f9bed2d89eea81760bbf91bd50.html | 65 + .../b0a9b7e0022a3038de42a18fa35d2ca221493be3.html | 233 + .../c76d7c0a5266afc747192fa55f565c32d68cad43.html | 34 + .../e19bf8c1cbd0b6e0a4dc5b393ebae8b12c997b87.html | 39 + .../e40ade36f579e1b404880014868c91569f374f39.html | 31 + git/sbm-suckless/file/.gitignore.html | 13 + git/sbm-suckless/file/Makefile.html | 36 + git/sbm-suckless/file/README.html | 78 + git/sbm-suckless/file/TODO.html | 21 + git/sbm-suckless/file/bm-sync.html | 229 + git/sbm-suckless/file/bm.html | 108 + git/sbm-suckless/file/bmks.html | 128 + git/sbm-suckless/file/config.mk.html | 15 + git/sbm-suckless/file/test.sh.html | 292 + git/sbm-suckless/file/usertags.html | 21 + git/sbm-suckless/files.html | 11 + git/sbm-suckless/index.html | 11 + git/sbm-suckless/refs.html | 11 + .../16e0f73443be4543cab396aab30172e1ac0af745.html | 58 + .../7a399ae866bd5c85c582fb61f789896faf84c91d.html | 36 + .../9508b6b6738e3db693459a000f9fcbc72a858562.html | 3091 ++++ .../f2f0e853d51c3fa0ff083c5b1535a1943964108b.html | 33 + git/sbm-sync/file/.gitattributes.html | 13 + git/sbm-sync/file/.github/FUNDING.yml.html | 11 + git/sbm-sync/file/.gitignore.html | 14 + git/sbm-sync/file/LICENSE.html | 672 + git/sbm-sync/file/README.md.html | 134 + git/sbm-sync/file/contrib/nginx.conf.html | 24 + git/sbm-sync/file/contrib/sbm-sync.service.html | 36 + git/sbm-sync/file/go.mod.html | 17 + git/sbm-sync/file/go.sum.html | 14 + git/sbm-sync/file/main.go.html | 611 + git/sbm-sync/file/merge.go.html | 133 + git/sbm-sync/file/merge_test.go.html | 202 + git/sbm-sync/file/pages.html.html | 143 + git/sbm-sync/file/server_test.go.html | 467 + git/sbm-sync/file/store.go.html | 409 + git/sbm-sync/file/stripe.go.html | 173 + ...ocalAdditionsStay-20260921033717-1828.fail.html | 28 + ...yWithoutConflict-20260921033649-16892.fail.html | 28 + git/sbm-sync/files.html | 11 + git/sbm-sync/index.html | 11 + git/sbm-sync/refs.html | 11 + .../20dcf6104b65502609ddd5de50e595764c30bbc6.html | 2 +- .../23925831b99a00491d80ccc0165179aab00bd334.html | 137 + .../2d25f2cd6c9f9d466d17c03d49d3c63cef601ec9.html | 208 + .../33b169c7d212442c253867b8cedcd7f4eaf3400d.html | 136 + .../35e13666096ac3d48031222e663c868c835f1209.html | 2 +- .../3f07452c2624482eb2511a97664dfc78c00e1103.html | 28 + .../46fb4edfb1267cd0ab816329fe87575c55a4a373.html | 404 + .../48da2ae40401b3e1c91cc044f03f1fcb2eb46856.html | 2 +- .../497f1961ad921c08c381e53445d3361573065665.html | 223 + .../4ec93f33bfb00c01530c4aeafcf7cf8d3efe5ab9.html | 2 +- .../527ba89a9c50cf8ed41030628858f8e08d1fca0b.html | 2 +- .../53b04640ebaf5ce0bc85ee04df91637078e82482.html | 2 +- .../54eff69143e540758de47ff5762dd89a8f391a8d.html | 2 +- .../55cccc49b46fdae17ffb4d6d35b6662a1547e112.html | 2130 +++ .../5aab41504e05ad1fc9a3a168be169eb27f8148bd.html | 366 + .../5dcddce1a09697ef5f5667a5121a9c3b7a71cd8d.html | 55 + .../5f825e6db438ce06f8abb759d529ac39068d0819.html | 380 + .../5fc6905438410e1f416088637d4701aee947c0c5.html | 33 + .../64867ada81fd939d50111898fd1f17b7521dfcc0.html | 76 + .../65906dd853842c8b739b45c0f54f3f07783a4f95.html | 2 +- .../66ca56ab25c837676d12410aacdc6497e3376372.html | 34 + .../73a2cfb70e9595ff30364c46be96b8cfbcd90a9e.html | 2 +- .../7c0b4c022cb883227066ce4342633a975f47b57c.html | 2 +- .../7e22e2783a158872b737c853a45ec03131603681.html | 77 + .../7ead489d91374feee9f9896db69b95deb2f48f4d.html | 705 + .../805d1e939219754e77114aeb9bddb75144e941b3.html | 2 +- .../84d1f87d0807097581665554d770a6bf7f437662.html | 2 +- .../850dc47b1ecfa0ce8f9e47f99a052b2bc9431a89.html | 180 + .../85303225927cb04a2d20ab64374cda69f3a71aa6.html | 2 +- .../a2a5d76a0cc524589a272390055c2ebc1e5800bd.html | 2 +- .../a6f35d3f02f3acea880d51d70e2b5adfddd53878.html | 2 +- .../a96023c55ebc96a86e09d82609fa7e415d96e298.html | 37 + .../ab1adc5c042505f9bed2d89eea81760bbf91bd50.html | 2 +- .../ab2df90d473319600f867e1b79890b2bcee5aa9c.html | 146 + .../b0a9b7e0022a3038de42a18fa35d2ca221493be3.html | 2 +- .../b39612b0f1cb79ca0d8d9add091b673d19fe0460.html | 2 +- .../c76d7c0a5266afc747192fa55f565c32d68cad43.html | 2 +- .../cc4c8606b9cbb79140dc466d00b25e8c62dbfb2d.html | 477 + .../d3653ad6e6cb55998ba0609b6c6fa8fbf4c8299d.html | 33 + .../e19bf8c1cbd0b6e0a4dc5b393ebae8b12c997b87.html | 2 +- .../e40ade36f579e1b404880014868c91569f374f39.html | 2 +- .../ecdecef7bea290b3642e1ba2f84322a8e2d12a68.html | 2 +- .../ee81288af3c599b8d3f4a9e9a8fc770ab5903193.html | 181 + .../f8e81eb7bf080ab88a219efb71d2561364cde135.html | 84 + .../fc9b3e85a7c191061b2e246d496f1dc772d5ffe7.html | 28 + git/sbm/file/.gitignore.html | 8 +- git/sbm/file/LICENSE.html | 672 + git/sbm/file/Makefile.html | 8 +- git/sbm/file/README.html | 485 +- git/sbm/file/TODO.html | 6 +- git/sbm/file/bm-check.html | 2 +- git/sbm/file/bm-commit.html | 2 +- git/sbm/file/bm-html.html | 2 +- git/sbm/file/bm-import.html | 254 +- git/sbm/file/bm-migrate.html | 2 +- git/sbm/file/bm-sync.html | 258 + git/sbm/file/bm-title.html | 2 +- git/sbm/file/bm-watch.html | 97 + git/sbm/file/bm.html | 1292 +- git/sbm/file/config.mk.html | 8 +- git/sbm/file/contrib/macos/Info.plist.html | 38 + git/sbm/file/contrib/macos/build-dmg.sh.html | 73 + git/sbm/file/contrib/macos/sbm-run.html | 22 + git/sbm/file/contrib/macos/sbm.html | 24 + .../file/contrib/windows/build-installer.sh.html | 64 + git/sbm/file/contrib/windows/fzf.html | 33 + git/sbm/file/contrib/windows/sbm-setup.html | 44 + git/sbm/file/contrib/windows/sbm.nsi.html | 195 + git/sbm/file/contrib/windows/sbm.sh.html | 21 + git/sbm/file/engines.html | 2 +- git/sbm/file/packaging/README.md.html | 254 + git/sbm/file/packaging/alpine/APKBUILD.html | 65 + git/sbm/file/packaging/arch/sbm-git/PKGBUILD.html | 77 + git/sbm/file/packaging/arch/sbm/PKGBUILD.html | 69 + git/sbm/file/packaging/chocolatey/sbm.nuspec.html | 86 + .../chocolatey/tools/chocolateyInstall.ps1.html | 176 + .../chocolatey/tools/chocolateyUninstall.ps1.html | 32 + git/sbm/file/packaging/debian/apt/README.md.html | 139 + .../file/packaging/debian/debian/changelog.html | 18 + git/sbm/file/packaging/debian/debian/control.html | 55 + .../file/packaging/debian/debian/copyright.html | 33 + git/sbm/file/packaging/debian/debian/docs.html | 12 + git/sbm/file/packaging/debian/debian/examples.html | 12 + git/sbm/file/packaging/debian/debian/rules.html | 29 + .../packaging/debian/debian/source/format.html | 11 + .../packaging/debian/debian/tests/control.html | 19 + .../file/packaging/debian/debian/tests/smoke.html | 38 + .../debian/debian/tests/upstream-suite.html | 18 + .../packaging/debian/debian/upstream/metadata.html | 15 + git/sbm/file/packaging/debian/debian/watch.html | 14 + .../packaging/freebsd/deskutils/sbm/Makefile.html | 75 + .../packaging/freebsd/deskutils/sbm/distinfo.html | 13 + .../packaging/freebsd/deskutils/sbm/pkg-descr.html | 25 + .../packaging/freebsd/deskutils/sbm/pkg-plist.html | 22 + .../gentoo/app-misc/sbm/metadata.xml.html | 49 + .../gentoo/app-misc/sbm/sbm-0.3.ebuild.html | 82 + .../gentoo/app-misc/sbm/sbm-9999.ebuild.html | 66 + git/sbm/file/packaging/guix/sbm.scm.html | 143 + git/sbm/file/packaging/homebrew/sbm.rb.html | 86 + git/sbm/file/packaging/macports/Portfile.html | 95 + git/sbm/file/packaging/nix/default.nix.html | 116 + git/sbm/file/packaging/nix/flake.nix.html | 44 + git/sbm/file/packaging/rpm/sbm.spec.html | 98 + git/sbm/file/packaging/scoop/sbm.json.html | 79 + .../file/packaging/void/srcpkgs/sbm/template.html | 54 + git/sbm/file/test/fakefzf.html | 2 +- git/sbm/file/test/fakemenu.html | 2 +- git/sbm/file/test/fixtures/chromium.json.html | 2 +- git/sbm/file/test/fixtures/netscape.html.html | 2 +- git/sbm/file/test/run.sh.html | 2021 ++- git/sbm/file/usertags.html | 2 +- git/sbm/files.html | 4 +- git/sbm/index.html | 4 +- git/sbm/refs.html | 4 +- .../0f086a9a1f2a4ce683b0a75bbfea481e3b6b3205.html | 25 + .../2925a3853d109ab3fec04c5cd559a23d0ce7c0b8.html | 75 + .../2e539051cb769e0086b04056a1a63852b5f83020.html | 152 + .../3298922ae53795654adf908ff46691bdb55c8a3d.html | 408 + .../331e3e5db01c2534e02e82810ee5a180eef7b851.html | 17404 +++++++++++++++++++ .../40b2679948d4356b4fd5a48781ec409437eef4ab.html | 340 + .../430d2318735dbc0aeb73d3c73180634dbe7244c6.html | 276 + .../5072d6216ebac1de4075d44aa0a6bf9a2593a15b.html | 1413 ++ .../50c110acefe403704086b3a008179840c0c7908e.html | 37 + .../5511145adf857c684382bef980f94fed457228a4.html | 3197 ++++ .../7e1fe5c203ee7184fea1591ec27ac6271af522d2.html | 977 ++ .../7ebbd3f6e900b44597c50d328870041f2e2ac6f6.html | 61 + .../99ec7e2e0b756301359cddd1d5478e18e84c3f8c.html | 189 + .../9c7ec98adcef01b6d6b7f768fe7280584d1426ea.html | 6007 +++++++ .../9db1bfdb31a73c0c3f92a9d286223a6e60b69128.html | 1526 ++ .../a8b978852782b4b63d4ab9627609ef16203f9dcc.html | 42 + .../b055da9c9850324f4ef0bf408dc438844a566e12.html | 197 + .../b1ea38063411586741613b9daa5d53194deadabf.html | 36 + .../b96bf0f91682cf2c75efb7e01c5898902722b471.html | 1046 ++ .../ba4d643eca98ca3763b334f356d3bd12b085dd5a.html | 1509 ++ .../c545960ed4b45d42095e0157b5b57e252f19f7e0.html | 668 + .../ca036bce54a881736cf8a97caeb845875f522a50.html | 84 + .../f9ace057f8b888cd5112fd3845a062f6ff8f2c7e.html | 228 + git/subplz-web/file/.env.example.html | 101 + git/subplz-web/file/.gitattributes.html | 11 + git/subplz-web/file/.github/FUNDING.yml.html | 11 + git/subplz-web/file/.gitignore.html | 43 + git/subplz-web/file/LICENSE.html | 672 + git/subplz-web/file/NOTICE.html | 48 + git/subplz-web/file/README.md.html | 468 + git/subplz-web/file/backend/__init__.py.html | 11 + git/subplz-web/file/backend/accounts.py.html | 115 + git/subplz-web/file/backend/aligner.py.html | 336 + git/subplz-web/file/backend/api.py.html | 896 + git/subplz-web/file/backend/auth.py.html | 96 + git/subplz-web/file/backend/billing.py.html | 135 + git/subplz-web/file/backend/chapters.py.html | 255 + git/subplz-web/file/backend/convert.py.html | 272 + git/subplz-web/file/backend/db.py.html | 292 + git/subplz-web/file/backend/detect.py.html | 189 + git/subplz-web/file/backend/gen_languages.py.html | 77 + git/subplz-web/file/backend/languages.json.html | 503 + git/subplz-web/file/backend/languages.py.html | 73 + git/subplz-web/file/backend/mailer.py.html | 74 + git/subplz-web/file/backend/main.py.html | 178 + git/subplz-web/file/backend/matching.py.html | 367 + git/subplz-web/file/backend/payments.py.html | 283 + git/subplz-web/file/backend/pricing.py.html | 141 + git/subplz-web/file/backend/queue.py.html | 112 + git/subplz-web/file/backend/render.py.html | 281 + git/subplz-web/file/backend/retention.py.html | 94 + git/subplz-web/file/backend/runner.py.html | 695 + git/subplz-web/file/backend/settings.py.html | 157 + git/subplz-web/file/backend/storage.py.html | 180 + git/subplz-web/file/frontend/app.js.html | 1014 ++ git/subplz-web/file/frontend/engine/align.js.html | 617 + .../file/frontend/engine/align.worker.js.html | 21 + git/subplz-web/file/frontend/engine/asr.js.html | 85 + git/subplz-web/file/frontend/engine/book.js.html | 280 + git/subplz-web/file/frontend/engine/epub.js.html | 386 + git/subplz-web/file/frontend/engine/job.js.html | 261 + .../file/frontend/engine/languages.js.html | 30 + git/subplz-web/file/frontend/engine/media.js.html | 121 + git/subplz-web/file/frontend/index.html.html | 258 + git/subplz-web/file/frontend/lab.html.html | 109 + git/subplz-web/file/frontend/style.css.html | 379 + git/subplz-web/file/frontend/terms.html.html | 108 + git/subplz-web/file/package-lock.json.html | 642 + git/subplz-web/file/package.json.html | 21 + git/subplz-web/file/requirements-dev.txt.html | 13 + git/subplz-web/file/requirements.txt.html | 30 + git/subplz-web/file/run.ps1.html | 107 + git/subplz-web/file/tests/__init__.py.html | 11 + git/subplz-web/file/tests/conftest.py.html | 170 + .../file/tests/engine/align.test.mjs.html | 131 + .../file/tests/engine/epub.test.mjs.html | 262 + .../file/tests/engine/mobi.test.mjs.html | 55 + git/subplz-web/file/tests/test_auth.py.html | 149 + git/subplz-web/file/tests/test_billing.py.html | 297 + git/subplz-web/file/tests/test_local_jobs.py.html | 117 + git/subplz-web/file/tests/test_migration.py.html | 91 + git/subplz-web/file/tests/test_retention.py.html | 90 + .../file/tests/test_runner_probe.py.html | 38 + .../file/tests/test_static_headers.py.html | 35 + git/subplz-web/file/tools/client.py.html | 269 + git/subplz-web/file/tools/fetch_vendor.py.html | 140 + git/subplz-web/file/tools/set-secret.sh.html | 38 + git/subplz-web/file/worker.py.html | 75 + git/subplz-web/files.html | 11 + git/subplz-web/index.html | 11 + git/subplz-web/refs.html | 11 + .../0125779ca4e6dff2507cc566c3ad57c213cac5ed.html | 1899 ++ .../0796dfb62a55526a1e0455103b6b12dc98ea6aa0.html | 747 + .../0fddd77f9c132aa6f38f9e16bed1bde05a75cea8.html | 136 + .../11d2e057f70a69395751bcd93236085060b47144.html | 262 + .../1d4a390c79ea2a0270cc933886b5d3a6cb2d5257.html | 89 + .../2555c531690ddd578656af1da2d6d90d3c5f9dcb.html | 1796 ++ .../2fdbdbb3e1585db5e0bc3468fe7fbefa72672428.html | 32 + .../3c439b08848fbc9bfe12e481443af15bc2309860.html | 541 + .../4545f4ebea19ebc41bc6da5a6eaadd58038ac722.html | 409 + .../48015e5da7640d1266e5720cfce430cb3371e37f.html | 213 + .../4a71c173c4ebdf69d931fc729ca3dfdb897bc7a6.html | 25 + .../4c411ddf56d3a64af65ce2810cefc5b8f4cf2e3d.html | 12038 +++++++++++++ .../5645f085057423d88412b526fe14ec99fc0c0549.html | 70 + .../5d84efb11393d51b16065840e69390d2a3a52433.html | 47 + .../62dca60b0bd3bba8bf74e6543ba635f15be9e72e.html | 277 + .../780551f1f74eeafb670dfabef73b6b972b2c4447.html | 33 + .../8044cb2174364c81baf3971d8f438dfcfe5bcf1d.html | 493 + .../81346df6b289c2fbd8e9ea8a3f9d3dbb560b9846.html | 34 + .../9524f072333c9f8fda883416f6edb5e5f10e6d04.html | 947 + .../95d0437e0233fe956840a2719005ed74a2458df5.html | 287 + .../a367ba62b43a69ae64dbcc1876cf01ad3dab1c50.html | 89 + .../ab44760809689d747c1e762fd83223f3840c57ca.html | 67 + .../cb5d68ebdf7570ec19a53d07a173ed320fca918b.html | 48 + .../e29dcb9ccc0a344009a3b965d1c9dfd398cbe8d4.html | 1798 ++ .../e690e1b4a03f40e9d46a1d760bcba531fa12732f.html | 693 + .../e9e86f61d6f8b97b0caf26be6f835f7b6fc8704e.html | 34 + .../ebfef576ba74b328666d7b16bd06bb1f73098177.html | 70 + .../f07eecbd406d67b0c93ee63736cd0c65a75583b0.html | 115 + .../f43ca2ac4ccd6d46d744511eaa8d2afccf835fda.html | 450 + .../f4b45997353b566bcacc80a8b2ba0df058b304e6.html | 52 + .../fa82b66f64f36667970137cee9c6d3e7a4f068b7.html | 42 + git/subread-android/file/.gitattributes.html | 15 + git/subread-android/file/.github/FUNDING.yml.html | 11 + .../file/.github/workflows/build.yml.html | 102 + git/subread-android/file/.gitignore.html | 34 + git/subread-android/file/.gitmodules.html | 14 + git/subread-android/file/LICENSE.html | 672 + git/subread-android/file/NOTICE.html | 49 + git/subread-android/file/README.md.html | 141 + git/subread-android/file/app/build.gradle.kts.html | 121 + .../space/subread/app/AlignIntentTest.kt.html | 175 + .../space/subread/app/CapabilitiesTest.kt.html | 66 + .../space/subread/app/OnDeviceJobTest.kt.html | 89 + .../space/subread/app/StartButtonTest.kt.html | 94 + .../space/subread/app/VideoMakerTest.kt.html | 181 + .../file/app/src/main/AndroidManifest.xml.html | 48 + .../file/app/src/main/cpp/CMakeLists.txt.html | 44 + .../file/app/src/main/cpp/whisper_jni.c.html | 163 + .../kotlin/space/subread/app/MainActivity.kt.html | 613 + .../space/subread/app/audio/AudioDecoder.kt.html | 228 + .../space/subread/app/intent/AlignRequest.kt.html | 91 + .../main/kotlin/space/subread/app/job/Job.kt.html | 150 + .../space/subread/app/job/TranscriptStore.kt.html | 130 + .../space/subread/app/video/VideoExport.kt.html | 61 + .../space/subread/app/video/VideoMaker.kt.html | 489 + .../space/subread/app/whisper/Whisper.kt.html | 137 + .../res/drawable/ic_launcher_foreground.xml.html | 17 + .../res/mipmap-anydpi-v26/ic_launcher.xml.html | 16 + .../mipmap-anydpi-v26/ic_launcher_round.xml.html | 16 + .../file/app/src/main/res/values/colors.xml.html | 14 + .../file/app/src/main/res/values/strings.xml.html | 14 + .../file/app/src/main/res/values/themes.xml.html | 19 + .../file/app/src/main/res/xml/file_paths.xml.html | 15 + .../subread/app/intent/AlignRequestsTest.kt.html | 82 + git/subread-android/file/build.gradle.kts.html | 16 + .../file/core/build.gradle.kts.html | 43 + .../kotlin/space/subread/core/AlignSub.kt.html | 227 + .../main/kotlin/space/subread/core/Aligner.kt.html | 62 + .../space/subread/core/AnchoredAligner.kt.html | 252 + .../kotlin/space/subread/core/BookAligner.kt.html | 108 + .../kotlin/space/subread/core/BookText.kt.html | 165 + .../main/kotlin/space/subread/core/Gotoh.kt.html | 182 + .../kotlin/space/subread/core/H264Still.kt.html | 237 + .../kotlin/space/subread/core/Language.kt.html | 76 + .../main/kotlin/space/subread/core/Model.kt.html | 33 + .../src/main/kotlin/space/subread/core/Srt.kt.html | 40 + .../main/kotlin/space/subread/core/Subs.kt.html | 179 + .../kotlin/space/subread/core/BookTest.kt.html | 204 + .../kotlin/space/subread/core/DebugDiff.kt.html | 73 + .../kotlin/space/subread/core/ExcerptTest.kt.html | 51 + .../kotlin/space/subread/core/FullBookTest.kt.html | 68 + .../kotlin/space/subread/core/GoldenTest.kt.html | 206 + .../space/subread/core/H264StillTest.kt.html | 152 + git/subread-android/file/docs/intent-api.md.html | 86 + .../metadata/android/en-US/changelogs/10.txt.html | 11 + .../metadata/android/en-US/changelogs/11.txt.html | 11 + .../metadata/android/en-US/changelogs/7.txt.html | 11 + .../metadata/android/en-US/changelogs/8.txt.html | 11 + .../metadata/android/en-US/changelogs/9.txt.html | 11 + .../android/en-US/full_description.txt.html | 35 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + .../file/fdroid/space.subread.app.yml.html | 59 + git/subread-android/file/gradle.properties.html | 15 + .../file/gradle/libs.versions.toml.html | 48 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/subread-android/file/gradlew.bat.html | 104 + git/subread-android/file/gradlew.html | 261 + git/subread-android/file/play/listing.md.html | 102 + git/subread-android/file/settings.gradle.kts.html | 42 + .../file/tools/make_fullbook.py.html | 40 + git/subread-android/file/tools/make_golden.py.html | 228 + git/subread-android/file/tools/write_res.py.html | 75 + git/subread-android/files.html | 11 + git/subread-android/index.html | 11 + git/subread-android/refs.html | 11 + .../13eb4ddbe45ec03d34d335cd8e60f1500065d3ff.html | 35 + .../366948cd2fd0a00f9ff8ef44f0d8b8c83ae0e134.html | 3566 ++++ git/subread-dictionary/file/.gitignore.html | 30 + git/subread-dictionary/file/LICENSE.html | 672 + git/subread-dictionary/file/README.md.html | 80 + .../file/app/build.gradle.kts.html | 80 + .../subread/dictionary/DictionariesTest.kt.html | 111 + .../file/app/src/main/AndroidManifest.xml.html | 64 + .../kotlin/space/subread/dictionary/Audio.kt.html | 130 + .../space/subread/dictionary/Dictionaries.kt.html | 174 + .../subread/dictionary/LookupActivity.kt.html | 260 + .../space/subread/dictionary/MainActivity.kt.html | 291 + .../kotlin/space/subread/dictionary/Store.kt.html | 53 + .../file/app/src/main/res/drawable/icon.xml.html | 19 + .../file/app/src/main/res/values/strings.xml.html | 72 + .../file/app/src/main/res/values/styles.xml.html | 22 + git/subread-dictionary/file/build.gradle.kts.html | 15 + .../file/core/build.gradle.kts.html | 41 + .../subread/dictionary/core/Deinflector.kt.html | 293 + .../space/subread/dictionary/core/Glossary.kt.html | 152 + .../space/subread/dictionary/core/Kana.kt.html | 28 + .../space/subread/dictionary/core/Lookup.kt.html | 80 + .../space/subread/dictionary/core/Yomitan.kt.html | 181 + .../space/subread/dictionary/core/CoreTest.kt.html | 303 + git/subread-dictionary/file/gradle.properties.html | 15 + .../file/gradle/libs.versions.toml.html | 37 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/subread-dictionary/file/gradlew.bat.html | 104 + git/subread-dictionary/file/gradlew.html | 261 + .../file/settings.gradle.kts.html | 36 + git/subread-dictionary/files.html | 11 + git/subread-dictionary/index.html | 11 + git/subread-dictionary/refs.html | 11 + .../0fdf3e2971322525deea4288c62d94bd1cbde1e3.html | 42 + .../4eb003876942dd6ba73faa3d186cba2f94a2d5e1.html | 1306 ++ .../6d6a99eb59ddb0f658216c246c076ff56fb1a29e.html | 61 + .../f762f21e7438452b7a3b99355e014d3ca11924c9.html | 25 + .../file/.github/FUNDING.yml.html | 11 + git/subread-extension/file/.gitignore.html | 12 + git/subread-extension/file/LICENSE.html | 672 + git/subread-extension/file/README.md.html | 57 + git/subread-extension/file/manifest.json.html | 32 + git/subread-extension/file/popup.html.html | 33 + git/subread-extension/file/src/content.css.html | 47 + git/subread-extension/file/src/content.js.html | 182 + git/subread-extension/file/src/srt.js.html | 64 + git/subread-extension/file/store/listing.md.html | 61 + .../file/tests/mock/server.py.html | 52 + git/subread-extension/file/tests/srt.test.cjs.html | 66 + git/subread-extension/file/tools/pack.py.html | 33 + git/subread-extension/files.html | 11 + git/subread-extension/index.html | 11 + git/subread-extension/refs.html | 11 + .../14b481be6f3f492fe76331ac766aafecc0f5856c.html | 2861 +++ .../232b3124bb54c729b506ef16de06f1c56f828aec.html | 588 + .../23e81d7038432d1764167f6635ae183ac05865f0.html | 291 + .../417e3e3f8355d5467e872bc092b3970ebfa94bc4.html | 63 + .../596efd13c8c4523e998043a602da8f80eee2a739.html | 33 + .../68dbeb567c7baf3e75d26221f44c22f10d196998.html | 62 + .../7d3a96c52c2f50f3e362c95cbe1c9d27414dfc6f.html | 147 + .../986cd86dfdfc329e514efc4b73f08a52c44c195b.html | 394 + .../9ceaf74ad53d246857d910e35b713c9e4b3d71b5.html | 84 + .../d45b05cfa8b789ce5b13778a7e4f01ae03a8ebbf.html | 25 + .../da774d424169a253cf7cbe77414b21821df4362a.html | 63 + .../f97704e83deff38bf92125aff677e4f508caa3e1.html | 129 + git/subread-overlay/file/.gitignore.html | 30 + git/subread-overlay/file/LICENSE.html | 672 + git/subread-overlay/file/README.md.html | 145 + git/subread-overlay/file/app/build.gradle.kts.html | 80 + .../space/subread/overlay/OverlayTest.kt.html | 318 + .../subread/overlay/PlayerProviderTest.kt.html | 104 + .../file/app/src/main/AndroidManifest.xml.html | 68 + .../kotlin/space/subread/overlay/Follower.kt.html | 137 + .../space/subread/overlay/MainActivity.kt.html | 267 + .../space/subread/overlay/MediaListener.kt.html | 264 + .../space/subread/overlay/OverlayView.kt.html | 269 + .../space/subread/overlay/PlayerProvider.kt.html | 141 + .../kotlin/space/subread/overlay/Store.kt.html | 94 + .../kotlin/space/subread/overlay/Words.kt.html | 42 + .../file/app/src/main/res/drawable/icon.xml.html | 18 + .../file/app/src/main/res/drawable/panel.xml.html | 16 + .../file/app/src/main/res/values/strings.xml.html | 58 + git/subread-overlay/file/build.gradle.kts.html | 15 + .../file/core/build.gradle.kts.html | 40 + .../space/subread/overlay/core/CueIndex.kt.html | 48 + .../space/subread/overlay/core/LiveLines.kt.html | 48 + .../space/subread/overlay/core/PlayClock.kt.html | 40 + .../kotlin/space/subread/overlay/core/Srt.kt.html | 57 + .../space/subread/overlay/core/CoreTest.kt.html | 156 + .../subread/overlay/core/LiveLinesTest.kt.html | 69 + .../metadata/android/en-US/changelogs/2.txt.html | 11 + .../metadata/android/en-US/changelogs/3.txt.html | 11 + .../metadata/android/en-US/changelogs/4.txt.html | 11 + .../android/en-US/full_description.txt.html | 30 + .../android/en-US/short_description.txt.html | 11 + .../fastlane/metadata/android/en-US/title.txt.html | 11 + .../file/fdroid/space.subread.overlay.yml.html | 49 + git/subread-overlay/file/gradle.properties.html | 15 + .../file/gradle/libs.versions.toml.html | 33 + .../gradle/wrapper/gradle-wrapper.properties.html | 19 + git/subread-overlay/file/gradlew.bat.html | 104 + git/subread-overlay/file/gradlew.html | 261 + git/subread-overlay/file/settings.gradle.kts.html | 36 + git/subread-overlay/files.html | 11 + git/subread-overlay/index.html | 11 + git/subread-overlay/refs.html | 11 + .../2d20e2fdac53fc77e64f0f82ae9f25f734ae21ff.html | 900 + .../58dc81ca8062196545c5b7226037691c6964429d.html | 835 + .../95cfc6087391a2451dd6aa2d7049cbfda3a1677f.html | 1147 ++ .../a1bd68d14754479363569dbf647bbcb9e1d121f2.html | 911 + .../b72a96213c16460b2c896a3f1c64d27a75322fdd.html | 57 + git/subread.koplugin/file/.busted.html | 22 + git/subread.koplugin/file/.gitattributes.html | 11 + git/subread.koplugin/file/LICENSE.html | 671 + git/subread.koplugin/file/README.md.html | 153 + git/subread.koplugin/file/_meta.lua.html | 23 + git/subread.koplugin/file/main.lua.html | 1189 ++ git/subread.koplugin/file/spec/clock_spec.lua.html | 136 + git/subread.koplugin/file/spec/cues_spec.lua.html | 150 + git/subread.koplugin/file/spec/run.lua.html | 55 + git/subread.koplugin/file/spec/srt_spec.lua.html | 141 + git/subread.koplugin/file/spec/text_spec.lua.html | 84 + .../file/subread/android_player.lua.html | 124 + git/subread.koplugin/file/subread/clock.lua.html | 123 + git/subread.koplugin/file/subread/cues.lua.html | 153 + .../file/subread/player_state.lua.html | 56 + git/subread.koplugin/file/subread/srt.lua.html | 130 + git/subread.koplugin/file/subread/text.lua.html | 177 + git/subread.koplugin/files.html | 11 + git/subread.koplugin/index.html | 11 + git/subread.koplugin/refs.html | 11 + 1014 files changed, 234532 insertions(+), 5019 deletions(-)
diff --git a/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html b/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html index 7a72673..d887301 100644 --- a/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html +++ b/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html @@ -1,41 +1,41 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain 4fb0f92f - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit 4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf -Spenser Truex <struex0@gmail.com> -2016-02-15 19:30:46 -0800 - -added space - -</pre><pre> README.md | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) -</pre><pre>diff --git a/README.md b/README.md -index f86df1a..e3c5246 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -44,7 +44,7 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` - ``` -<span class="d">->#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> -<span class="a">+> #f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain 4fb0f92f - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf +Spenser Truex <struex0@gmail.com> +2016-02-15 19:30:46 -0800 + +added space + +</pre><pre> README.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +</pre><pre>diff --git a/README.md b/README.md +index f86df1a..e3c5246 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -44,7 +44,7 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` + ``` +<span class="d">->#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> +<span class="a">+> #f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"</pre> +</div></body></html> diff --git a/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html b/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html index 370822c..7201323 100644 --- a/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html +++ b/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html @@ -1,48 +1,48 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain 75492a11 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit 75492a1107063b545c771844e77c34a852554350 -Spenser Truex <spensertruexonline@gmail.com> -2016-02-15 21:16:10 -0800 - -More pictures -</pre><pre> README.md | 2 ++ - 1 file changed, 2 insertions(+) -</pre><pre>diff --git a/README.md b/README.md -index ee16e8b..12dbd57 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -44,12 +44,14 @@ If everything runs smoothly you will be ready to Brainfuck. If there are issues</span> - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` -<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png)</span> - ``` - > #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>] - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` -<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/pound-f-notation.png)</span> - - #Debugging Brainfuck - </pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain 75492a11 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 75492a1107063b545c771844e77c34a852554350 +Spenser Truex <spensertruexonline@gmail.com> +2016-02-15 21:16:10 -0800 + +More pictures +</pre><pre> README.md | 2 ++ + 1 file changed, 2 insertions(+) +</pre><pre>diff --git a/README.md b/README.md +index ee16e8b..12dbd57 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -44,12 +44,14 @@ If everything runs smoothly you will be ready to Brainfuck. If there are issues</span> + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` +<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png)</span> + ``` + > #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>] + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` +<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/pound-f-notation.png)</span> + + #Debugging Brainfuck + </pre> +</div></body></html> diff --git a/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html b/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html index 5de0ecb..a08c201 100644 --- a/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html +++ b/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html @@ -1,40 +1,40 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain c9441131 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit c9441131467ba02e70e8cc466bfd2b769d09d985 -Spenser Truex <struex0@gmail.com> -2016-02-15 19:29:29 -0800 - -Readability block - -</pre><pre> README.md | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) -</pre><pre>diff --git a/README.md b/README.md -index f805791..f86df1a 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -42,7 +42,8 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" -<span class="d">-</span> -<span class="a">+```</span> -<span class="a">+```</span> - >#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code. - "� - - -</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain c9441131 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit c9441131467ba02e70e8cc466bfd2b769d09d985 +Spenser Truex <struex0@gmail.com> +2016-02-15 19:29:29 -0800 + +Readability block + +</pre><pre> README.md | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) +</pre><pre>diff --git a/README.md b/README.md +index f805791..f86df1a 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -42,7 +42,8 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" +<span class="d">-</span> +<span class="a">+```</span> +<span class="a">+```</span> + >#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code. + "� + + +</pre> +</div></body></html> diff --git a/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html b/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html index 57f2216..d19d039 100644 --- a/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html +++ b/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html @@ -1,74 +1,74 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain cecddfa6 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit cecddfa6396707ab94ada6098f3668145df39511 -Spenser Truex <spensertruexonline@gmail.com> -2016-03-03 12:13:46 -0800 - -clarification -</pre><pre> README.md | 12 ++++++------ - 1 file changed, 6 insertions(+), 6 deletions(-) -</pre><pre>diff --git a/README.md b/README.md -index dfba44d..2a253d3 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -26,7 +26,7 @@ These can be combined into a string such as the following "Hello World!" program</span> - #How to install: - - Make sure you have a Common Lisp implementation installed. I recommend [Steel Bank Common Lisp](http://www.sbcl.org/). - - [ASDF](https://common-lisp.net/project/asdf/) must be installed. Many Lisps come with it installed (including [SBCL](http://www.sbcl.org/)). -<span class="d">-- Install this code into your ASDF load directory. The default on linux is `~/common-lisp/`:</span> -<span class="a">+- Install this code into your ASDF *load directory*. The default on linux is usually `~/common-lisp/`:</span> - ``` - me@linux:~$ mkdir common-lisp - me@linux:~$ cd common-lisp -<span class="h">@@ -45,18 +45,18 @@ If you are unable to find where the ASDF load directory is, you may choose to lo</span> - > (asdf:load-system :brain) - ``` - -<span class="d">-In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the ASDF system.</span> -<span class="a">+In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the preinstalled ASDF system.</span> - #How to Use: -<span class="d">-If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"` inside of the Brainfuck code. Below they are both shown:</span> -<span class="a">+If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"`, or an unescaped literal backward slash `\` inside of the Brainfuck code. Below they are both shown:</span> - ``` -<span class="d">-> (brain:fuck ".+[.+] Please escape your \" characters!")</span> -<span class="a">+> (brain:fuck ".+[.+] Please escape your \" and \\ characters!")</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` - [](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png) - ``` -<span class="d">-> #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>]</span> -<span class="a">+> #f.+[.+] <This is not inside the Brainfuck code, nor is + or -.>[]</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" -<span class="h">@@ -111,7 +111,7 @@ Suppose you want to find information about the execution of `incf-byte` and `dec</span> - ``` - - #Bugs/TODO: -<span class="d">-- None right now, feedback is encouraged! My email is spensertruexonline@gmail.com</span> -<span class="a">+- Include instructions for users on Windows.</span> - - #Congratulations! - You have reached the end of this README. Please sumbit any feedback to me via email.</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain cecddfa6 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit cecddfa6396707ab94ada6098f3668145df39511 +Spenser Truex <spensertruexonline@gmail.com> +2016-03-03 12:13:46 -0800 + +clarification +</pre><pre> README.md | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) +</pre><pre>diff --git a/README.md b/README.md +index dfba44d..2a253d3 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -26,7 +26,7 @@ These can be combined into a string such as the following "Hello World!" program</span> + #How to install: + - Make sure you have a Common Lisp implementation installed. I recommend [Steel Bank Common Lisp](http://www.sbcl.org/). + - [ASDF](https://common-lisp.net/project/asdf/) must be installed. Many Lisps come with it installed (including [SBCL](http://www.sbcl.org/)). +<span class="d">-- Install this code into your ASDF load directory. The default on linux is `~/common-lisp/`:</span> +<span class="a">+- Install this code into your ASDF *load directory*. The default on linux is usually `~/common-lisp/`:</span> + ``` + me@linux:~$ mkdir common-lisp + me@linux:~$ cd common-lisp +<span class="h">@@ -45,18 +45,18 @@ If you are unable to find where the ASDF load directory is, you may choose to lo</span> + > (asdf:load-system :brain) + ``` + +<span class="d">-In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the ASDF system.</span> +<span class="a">+In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the preinstalled ASDF system.</span> + #How to Use: +<span class="d">-If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"` inside of the Brainfuck code. Below they are both shown:</span> +<span class="a">+If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"`, or an unescaped literal backward slash `\` inside of the Brainfuck code. Below they are both shown:</span> + ``` +<span class="d">-> (brain:fuck ".+[.+] Please escape your \" characters!")</span> +<span class="a">+> (brain:fuck ".+[.+] Please escape your \" and \\ characters!")</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` + [](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png) + ``` +<span class="d">-> #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>]</span> +<span class="a">+> #f.+[.+] <This is not inside the Brainfuck code, nor is + or -.>[]</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" +<span class="h">@@ -111,7 +111,7 @@ Suppose you want to find information about the execution of `incf-byte` and `dec</span> + ``` + + #Bugs/TODO: +<span class="d">-- None right now, feedback is encouraged! My email is spensertruexonline@gmail.com</span> +<span class="a">+- Include instructions for users on Windows.</span> + + #Congratulations! + You have reached the end of this README. Please sumbit any feedback to me via email.</pre> +</div></body></html> diff --git a/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html b/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html index e15bb7e..7d6fd42 100644 --- a/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html +++ b/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html @@ -1,36 +1,36 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain f3d17475 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit f3d174752dda70690cf073701c79901755cfc192 -Spenser Truex <struex0@gmail.com> -2016-02-14 09:39:05 -0800 - -added comment -</pre><pre> README.md | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) -</pre><pre>diff --git a/README.md b/README.md -index 1e19341..370dd65 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -48,7 +48,7 @@ Another example:</span> - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ - -<span class="d">-NIL</span> -<span class="a">+NIL ;note that NIL will always be returned by (interpret). The above text is written to *standard-output*</span> - ``` - #TODO: - - ASDF would be a better packaging system choice than the current arrangement.</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain f3d17475 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit f3d174752dda70690cf073701c79901755cfc192 +Spenser Truex <struex0@gmail.com> +2016-02-14 09:39:05 -0800 + +added comment +</pre><pre> README.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +</pre><pre>diff --git a/README.md b/README.md +index 1e19341..370dd65 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -48,7 +48,7 @@ Another example:</span> + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ + +<span class="d">-NIL</span> +<span class="a">+NIL ;note that NIL will always be returned by (interpret). The above text is written to *standard-output*</span> + ``` + #TODO: + - ASDF would be a better packaging system choice than the current arrangement.</pre> +</div></body></html> diff --git a/git/assistkey/commit/00e3cd71a896d0cacbf1a0416961a80538cd015f.html b/git/assistkey/commit/00e3cd71a896d0cacbf1a0416961a80538cd015f.html new file mode 100644 index 0000000..cbb7451 --- /dev/null +++ b/git/assistkey/commit/00e3cd71a896d0cacbf1a0416961a80538cd015f.html @@ -0,0 +1,1817 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>assistkey 00e3cd71 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>assistkey</h1><span class="desc">Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</span><p class="url">git clone https://github.com/equwal/assistkey</p><p><a href="../../assistkey/index.html">Log</a> | <a href="../../assistkey/files.html">Files</a> | <a href="../../assistkey/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 00e3cd71a896d0cacbf1a0416961a80538cd015f +truex <truex@equwal.com> +2026-09-21 03:36:26 -0700 + +Carry the apps of other makers and cut the home and recents screens + +The build for direct install carries three release APKs, not changed, +in app/src/full/assets/bundled, and installs them through the package +installer of Android: CLauncher v5.3.0 (GPL-3.0), Ink Recents 0.1.1 +(GPL-3.0) and Whisper 3.7 (MIT, the F-Droid build). NOTICE.txt names +each maker, licence, source and SHA-256. Rebind downloads nothing. The +Google Play build carries nothing, has no install permission, and opens +the page of the maker. + +The home screen of Rebind and its settings are gone. CLauncher is the +home screen. The recent-apps screen is gone too: it is now Ink Recents, +a program of its own. RecentsActivity is a stand-in with no screen that +opens Ink Recents, so that old bindings keep working. The app no longer +asks for usage access or KILL_BACKGROUND_PROCESSES. + +Hardware hacks: the double tap of Power is one row with two ways in, +the camera intent or the wallet intent. Channel titles name the gesture. + +The setup of a Power combination no longer says the same thing twice. + +Version 0.0.12-alpha, version code 12. + +</pre><pre> .gitignore | 2 + + CHANGELOG.md | 26 +- + README.md | 30 +- + app/src/full/AndroidManifest.xml | 9 + + app/src/full/assets/bundled/NOTICE.txt | 23 ++ + .../assets/bundled/clauncher-v5.3.0-universal.apk | Bin 0 -> 2073428 bytes + app/src/full/assets/bundled/ink-recents-0.1.1.apk | Bin 0 -> 2256640 bytes + .../assets/bundled/org.woheller69.whisper_37.apk | Bin 0 -> 22741441 bytes + .../java/dev/equwal/assistkey/bundle/Bundled.kt | 93 +++++ + app/src/main/AndroidManifest.xml | 37 +- + .../java/dev/equwal/assistkey/channel/Channel.kt | 12 +- + .../main/java/dev/equwal/assistkey/home/Apps.kt | 173 ---------- + .../java/dev/equwal/assistkey/home/HomeActivity.kt | 209 ------------ + .../equwal/assistkey/home/HomeSettingsActivity.kt | 107 ------ + .../dev/equwal/assistkey/home/RecentsActivity.kt | 378 +-------------------- + .../equwal/assistkey/setup/GuidedSetupActivity.kt | 4 +- + .../dev/equwal/assistkey/store/SettingsFile.kt | 1 - + .../equwal/assistkey/ui/ActionPickerActivity.kt | 18 +- + .../java/dev/equwal/assistkey/ui/BundledRows.kt | 28 ++ + .../java/dev/equwal/assistkey/ui/HacksActivity.kt | 8 +- + .../dev/equwal/assistkey/ui/HomeHubActivity.kt | 28 +- + .../java/dev/equwal/assistkey/ui/MainActivity.kt | 16 +- + .../dev/equwal/assistkey/ui/PermissionsActivity.kt | 9 +- + .../main/java/dev/equwal/assistkey/ui/Summary.kt | 10 +- + .../java/dev/equwal/assistkey/ui/VoiceActivity.kt | 14 +- + .../java/dev/equwal/assistkey/bundle/Bundled.kt | 49 +++ + .../dev/equwal/assistkey/home/RecentsOpenTest.kt | 30 -- + .../java/dev/equwal/assistkey/ui/SummaryTest.kt | 6 +- + gradle.properties | 4 +- + play/DECLARATIONS.md | 3 +- + play/LISTING.md | 5 +- + play/PRIVACY.md | 7 +- + 32 files changed, 310 insertions(+), 1029 deletions(-) +</pre><pre>diff --git a/.gitignore b/.gitignore +index 844ebd5..7940bdc 100644 +--- a/.gitignore ++++ b/.gitignore +<span class="h">@@ -2,6 +2,8 @@</span> + build/ + .gradle/ + *.apk +<span class="a">+# The unchanged release APKs of other makers that the full build carries.</span> +<span class="a">+!app/src/full/assets/bundled/*.apk</span> + *.aab + *.ap_ + *.dex +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 899411d..7659600 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +<span class="h">@@ -1,6 +1,30 @@</span> + # Changelog + +<span class="d">-## Unreleased</span> +<span class="a">+## 0.0.12-alpha - 2026-09-21</span> +<span class="a">+</span> +<span class="a">+Version code 12.</span> +<span class="a">+</span> +<span class="a">+- **Apps of other makers.** The build for direct install carries two apps and</span> +<span class="a">+ can install them: CLauncher v5.3.0 (the release APK of its maker, not</span> +<span class="a">+ changed) and Ink Recents 0.1.1 (open source). Home and recents lists them. Android asks before</span> +<span class="a">+ each install. The APKs are in the build, because Rebind has no internet</span> +<span class="a">+ permission. The Google Play build carries nothing and has no install</span> +<span class="a">+ permission: Google Play does not allow that here. It opens the page of the</span> +<span class="a">+ maker.</span> +<span class="a">+- Whisper 3.7 (speech to text on the device, MIT, the F-Droid build) is the</span> +<span class="a">+ third app that the build for direct install carries. Voice typing lists it.</span> +<span class="a">+- **The home screen of Rebind is gone**, with its settings screens. CLauncher</span> +<span class="a">+ is the home screen now, as its maker released it. An old settings file that</span> +<span class="a">+ has home screen settings still imports. Those settings are skipped.</span> +<span class="a">+- **Recent apps is a program of its own: Ink Recents** (open source,</span> +<span class="a">+ github.com/equwal/ink-recents). Rebind no longer has that screen, and no</span> +<span class="a">+ longer asks for usage access or `KILL_BACKGROUND_PROCESSES`. The Recent apps</span> +<span class="a">+ action opens Ink Recents. Old bindings keep working. Where Ink Recents is</span> +<span class="a">+ not installed, the action opens the screen that offers it.</span> +<span class="a">+- Hardware hacks: the double tap of Power is one row. Its way in is the camera</span> +<span class="a">+ intent or the wallet intent, as the device decides. There is no wallet</span> +<span class="a">+ button.</span> +<span class="a">+- The setup of a Power combination no longer says the same thing twice.</span> + + - **Hardware hacks** is a tile on the main screen with a screen of its own: + hold Power (assistant role), double tap Power (camera intent), the wallet +diff --git a/README.md b/README.md +index 543f3fd..04997e0 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -5,8 +5,8 @@ Formerly AssistKey. The package id, the repository and the code keep the old nam</span> + Remaps the hardware keys of an Android device - volume keys, the Power button, + and whatever else the device has - to arbitrary actions, with multi-tap, + press-and-hold and key combinations. Also: navigation in any mix of button bar, +<span class="d">-gestures and the Power key; a frontlight level below the system's floor; a</span> +<span class="d">-plain text home screen and recent-apps list made for e-ink.</span> +<span class="a">+gestures and the Power key; a frontlight level below the system's floor; and a</span> +<span class="a">+recent-apps list made for e-ink.</span> + + It runs on any Android 12+ device. The **Viwoods AiPaper Reader** is the first + device profile, because that is where it was built; see *Device profiles*. +<span class="h">@@ -154,16 +154,21 @@ The Viwoods voice prompt is not reusable. It uploads audio to the Viwoods cloud</span> + Debug builds carry `FakeRecognitionService`, which hears a fixed sentence, so + the whole chain can be tested on a bench with no voice. + +<span class="d">-## Home screen and recent apps</span> +<span class="a">+## Home, recent apps, speech: apps of other makers</span> + +<span class="d">-`home/`. The home screen is a clock, a few chosen apps and a search line; the</span> +<span class="d">-app list is hidden until you type, and a single match opens itself. It is</span> +<span class="d">-original code: the idea is shared with CLauncher/Olauncher, which are GPL-3.0</span> +<span class="d">-and therefore cannot be copied into this app. Disabled in the manifest until</span> +<span class="d">-switched on. Recent apps is a row of cards like the Android switcher, drawn for</span> +<span class="d">-e-ink: outlines, no animation, one jump per card. A card closes with a swipe up</span> +<span class="d">-when there is shell access, and shows the task picture the system keeps when</span> +<span class="d">-the shell is root (`/data/system_ce/0/snapshots`, owned by `system`).</span> +<span class="a">+Rebind has no recent-apps screen of its own. That screen is a program of its</span> +<span class="a">+own, open source: [Ink Recents](https://github.com/equwal/ink-recents). The</span> +<span class="a">+Recent apps action opens it (`dev.equwal.inkrecents.OPEN`). Where it is not</span> +<span class="a">+installed, the action opens the screen that offers it. `home/RecentsActivity`</span> +<span class="a">+is only that stand-in, and keeps the class name that old bindings hold.</span> +<span class="a">+</span> +<span class="a">+Rebind has no home screen of its own. The `full` build carries the release</span> +<span class="a">+APKs of other makers, not changed, in `app/src/full/assets/bundled/`, and</span> +<span class="a">+installs them with `REQUEST_INSTALL_PACKAGES`: CLauncher (home screen,</span> +<span class="a">+GPL-3.0), Ink Recents (GPL-3.0) and Whisper (speech to text, MIT).</span> +<span class="a">+`NOTICE.txt` in that folder names each maker, licence, source and SHA-256.</span> +<span class="a">+Android asks the user before each install. The `play` build carries nothing</span> +<span class="a">+and has no install permission. It opens the page of the maker.</span> + + ## Why it is shaped like this + +<span class="h">@@ -432,7 +437,8 @@ engine/ the gesture state machine and the accessibility service</span> + channel/ the key filter switch and the Power side doors (assistant, camera, wallet) + device/ device profiles + display/ the extra-dim light +<span class="d">-home/ the home screen and the recent-apps cards</span> +<span class="a">+home/ the stand-in that opens Ink Recents</span> +<span class="a">+bundle/ apps of other makers that the full build carries (per flavour)</span> + menu/ the menu of actions + shell/ shell access through Shizuku, and the managed Power button + voice/ voice typing: recognizer choice, the listening screen, text insert +diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml +index 597e623..5ade5cc 100644 +--- a/app/src/full/AndroidManifest.xml ++++ b/app/src/full/AndroidManifest.xml +<span class="h">@@ -2,11 +2,20 @@</span> + <!-- The `full` flavour only: everything Shizuku needs. The `play` flavour has none of it. --> + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + +<span class="a">+ <!-- Installs the apps of other makers that this build carries in its assets.</span> +<span class="a">+ Android asks the user before each install. Not in the Google Play build. --></span> +<span class="a">+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /></span> +<span class="a">+</span> + <queries> + <package android:name="moe.shizuku.privileged.api" /> + </queries> + + <application> +<span class="a">+ <!-- The package installer of Android answers an install request here. --></span> +<span class="a">+ <receiver</span> +<span class="a">+ android:name="dev.equwal.assistkey.bundle.Bundled$Result"</span> +<span class="a">+ android:exported="false" /></span> +<span class="a">+</span> + <!-- How Shizuku hands this app its binder. --> + <provider + android:name="rikka.shizuku.ShizukuProvider" +diff --git a/app/src/full/assets/bundled/NOTICE.txt b/app/src/full/assets/bundled/NOTICE.txt +new file mode 100644 +index 0000000..0b4bbc0 +--- /dev/null ++++ b/app/src/full/assets/bundled/NOTICE.txt +<span class="h">@@ -0,0 +1,23 @@</span> +<span class="a">+Apps of other makers in this folder</span> +<span class="a">+</span> +<span class="a">+Each file is the release APK of its maker, not changed in any way. It is a</span> +<span class="a">+separate program. Rebind only hands it to the package installer of Android.</span> +<span class="a">+</span> +<span class="a">+clauncher-v5.3.0-universal.apk</span> +<span class="a">+ CLauncher v5.3.0, by mlm-games</span> +<span class="a">+ Licence: GNU General Public License v3.0</span> +<span class="a">+ Source code and releases: https://github.com/mlm-games/CLauncher</span> +<span class="a">+ SHA-256: aedb78216822d5c8924eb125529f2dfa9140e4841b87ed309eca8f12f38956eb</span> +<span class="a">+</span> +<span class="a">+ink-recents-0.1.1.apk</span> +<span class="a">+ Ink Recents 0.1.1, by equwal</span> +<span class="a">+ Licence: GNU General Public License v3.0 or later</span> +<span class="a">+ Source code and releases: https://github.com/equwal/ink-recents</span> +<span class="a">+ SHA-256: f524150a25d7980224b93948133c5abee7c7bb4af3de9d7e721162fca5dd180b</span> +<span class="a">+</span> +<span class="a">+org.woheller69.whisper_37.apk</span> +<span class="a">+ Whisper 3.7, by woheller69, as F-Droid built and signed it</span> +<span class="a">+ Licence: MIT</span> +<span class="a">+ Source code: https://github.com/woheller69/whisperIME</span> +<span class="a">+ Release: https://f-droid.org/packages/org.woheller69.whisper/</span> +<span class="a">+ SHA-256: b7efb1f4377c7ecf870688b1f7d74ad201bff2bd8991a387316db3ab71353824</span> +diff --git a/app/src/full/assets/bundled/clauncher-v5.3.0-universal.apk b/app/src/full/assets/bundled/clauncher-v5.3.0-universal.apk +new file mode 100644 +index 0000000..c5051cd +Binary files /dev/null and b/app/src/full/assets/bundled/clauncher-v5.3.0-universal.apk differ +diff --git a/app/src/full/assets/bundled/ink-recents-0.1.1.apk b/app/src/full/assets/bundled/ink-recents-0.1.1.apk +new file mode 100644 +index 0000000..41bafc6 +Binary files /dev/null and b/app/src/full/assets/bundled/ink-recents-0.1.1.apk differ +diff --git a/app/src/full/assets/bundled/org.woheller69.whisper_37.apk b/app/src/full/assets/bundled/org.woheller69.whisper_37.apk +new file mode 100644 +index 0000000..e908ca2 +Binary files /dev/null and b/app/src/full/assets/bundled/org.woheller69.whisper_37.apk differ +diff --git a/app/src/full/java/dev/equwal/assistkey/bundle/Bundled.kt b/app/src/full/java/dev/equwal/assistkey/bundle/Bundled.kt +new file mode 100644 +index 0000000..d46ab37 +--- /dev/null ++++ b/app/src/full/java/dev/equwal/assistkey/bundle/Bundled.kt +<span class="h">@@ -0,0 +1,93 @@</span> +<span class="a">+package dev.equwal.assistkey.bundle</span> +<span class="a">+</span> +<span class="a">+import android.app.Activity</span> +<span class="a">+import android.app.PendingIntent</span> +<span class="a">+import android.content.BroadcastReceiver</span> +<span class="a">+import android.content.Context</span> +<span class="a">+import android.content.Intent</span> +<span class="a">+import android.content.pm.PackageInstaller</span> +<span class="a">+import android.widget.Toast</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * Apps of other makers that this build carries and can install.</span> +<span class="a">+ *</span> +<span class="a">+ * Each app is the APK that its maker released, byte for byte, in the assets of</span> +<span class="a">+ * this build. Rebind does not change it and does not download it: this app has</span> +<span class="a">+ * no internet permission. Android asks the user before each install.</span> +<span class="a">+ *</span> +<span class="a">+ * Only the build for direct install has this. Google Play does not allow an</span> +<span class="a">+ * app to install other apps unless that is its main purpose.</span> +<span class="a">+ */</span> +<span class="a">+object Bundled {</span> +<span class="a">+</span> +<span class="a">+ const val SUPPORTED = true</span> +<span class="a">+</span> +<span class="a">+ class Item(</span> +<span class="a">+ /** The screen that lists the app: HOME or VOICE. */</span> +<span class="a">+ val group: String,</span> +<span class="a">+ val title: String,</span> +<span class="a">+ val hint: String,</span> +<span class="a">+ val pkg: String,</span> +<span class="a">+ val version: String,</span> +<span class="a">+ val licence: String,</span> +<span class="a">+ /** Where the maker publishes the source code and the releases. */</span> +<span class="a">+ val source: String,</span> +<span class="a">+ internal val asset: String</span> +<span class="a">+ )</span> +<span class="a">+</span> +<span class="a">+ const val HOME = "home"</span> +<span class="a">+ const val VOICE = "voice"</span> +<span class="a">+</span> +<span class="a">+ val items = listOf(</span> +<span class="a">+ Item(</span> +<span class="a">+ HOME, "CLauncher", "Minimal home screen", "app.clauncher", "v5.3.0", "GPL-3.0",</span> +<span class="a">+ "https://github.com/mlm-games/CLauncher", "bundled/clauncher-v5.3.0-universal.apk"</span> +<span class="a">+ ),</span> +<span class="a">+ Item(</span> +<span class="a">+ HOME, "Ink Recents", "Recent apps, drawn for e-ink", "dev.equwal.inkrecents", "0.1.1", "GPL-3.0",</span> +<span class="a">+ "https://github.com/equwal/ink-recents", "bundled/ink-recents-0.1.1.apk"</span> +<span class="a">+ ),</span> +<span class="a">+ Item(</span> +<span class="a">+ VOICE, "Whisper", "Speech to text on the device", "org.woheller69.whisper", "3.7", "MIT",</span> +<span class="a">+ "https://f-droid.org/packages/org.woheller69.whisper/", "bundled/org.woheller69.whisper_37.apk"</span> +<span class="a">+ )</span> +<span class="a">+ )</span> +<span class="a">+</span> +<span class="a">+ /** Hands the APK to the package installer of Android. Android then asks the user. */</span> +<span class="a">+ fun install(a: Activity, item: Item) {</span> +<span class="a">+ runCatching {</span> +<span class="a">+ val installer = a.packageManager.packageInstaller</span> +<span class="a">+ val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)</span> +<span class="a">+ params.setAppPackageName(item.pkg)</span> +<span class="a">+ val id = installer.createSession(params)</span> +<span class="a">+ installer.openSession(id).use { session -></span> +<span class="a">+ a.assets.open(item.asset).use { input -></span> +<span class="a">+ session.openWrite("base.apk", 0, -1).use { out -></span> +<span class="a">+ input.copyTo(out)</span> +<span class="a">+ session.fsync(out)</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ val result = PendingIntent.getBroadcast(</span> +<span class="a">+ a, id, Intent(a, Result::class.java),</span> +<span class="a">+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT</span> +<span class="a">+ )</span> +<span class="a">+ session.commit(result.intentSender)</span> +<span class="a">+ }</span> +<span class="a">+ }.onFailure { Toast.makeText(a, "Could not start the install", Toast.LENGTH_LONG).show() }</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ /** Android answers here: first with the question for the user, then with the result. */</span> +<span class="a">+ class Result : BroadcastReceiver() {</span> +<span class="a">+ override fun onReceive(c: Context, i: Intent) {</span> +<span class="a">+ when (i.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {</span> +<span class="a">+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {</span> +<span class="a">+ @Suppress("DEPRECATION")</span> +<span class="a">+ val ask = i.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) ?: return</span> +<span class="a">+ runCatching { c.startActivity(ask.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }</span> +<span class="a">+ }</span> +<span class="a">+ PackageInstaller.STATUS_SUCCESS -> Toast.makeText(c, "Installed", Toast.LENGTH_SHORT).show()</span> +<span class="a">+ else -> Toast.makeText(c, "Not installed", Toast.LENGTH_LONG).show()</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml +index 1385157..3727bb5 100644 +--- a/app/src/main/AndroidManifest.xml ++++ b/app/src/main/AndroidManifest.xml +<span class="h">@@ -10,22 +10,12 @@</span> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" + tools:ignore="ProtectedPermissions" /> + +<span class="d">- <!--</span> +<span class="d">- Special access the user grants from a settings screen. Only the recent</span> +<span class="d">- apps list uses it, and only when there is no shell access to ask the</span> +<span class="d">- system directly.</span> +<span class="d">- --></span> +<span class="d">- <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"</span> +<span class="d">- tools:ignore="ProtectedPermissions" /></span> + + <!-- + Voice typing only. The speech recognition app does the recording; Android + still requires the app that asks for it to hold the permission. Requested + at run time, from the Voice typing screen, and never otherwise. + --> +<span class="d">- <!-- Recent apps: "close" ends the background processes of an app where</span> +<span class="d">- there is no shell access. A normal permission: Android grants it at install. --></span> +<span class="d">- <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" /></span> + + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + +<span class="h">@@ -49,6 +39,8 @@</span> + <intent> + <action android:name="android.speech.RecognitionService" /> + </intent> +<span class="a">+ <!-- Ink Recents, which the Recent apps action opens. --></span> +<span class="a">+ <package android:name="dev.equwal.inkrecents" /></span> + <package android:name="com.viwoods.launcher" /> + <package android:name="com.viwoods.viwoodsai" /> + <package android:name="com.viwoods.settings" /> +<span class="h">@@ -113,33 +105,14 @@</span> + android:launchMode="singleInstance" + android:taskAffinity="" + android:theme="@style/Theme.Invisible" /> +<span class="d">- <activity android:name=".home.HomeSettingsActivity" android:exported="false" /></span> + <activity + android:name=".home.RecentsActivity" + android:exported="false" + android:excludeFromRecents="true" +<span class="d">- android:launchMode="singleTask"</span> +<span class="d">- android:taskAffinity="dev.equwal.assistkey.recents" /></span> +<span class="a">+ android:noHistory="true"</span> +<span class="a">+ android:taskAffinity=""</span> +<span class="a">+ android:theme="@style/Theme.Invisible" /></span> + +<span class="d">- <!--</span> +<span class="d">- The home screen. Disabled until the user asks for it, so the app is</span> +<span class="d">- not a launcher candidate on anybody's device by default.</span> +<span class="d">- --></span> +<span class="d">- <activity</span> +<span class="d">- android:name=".home.HomeActivity"</span> +<span class="d">- android:enabled="false"</span> +<span class="d">- android:exported="true"</span> +<span class="d">- android:excludeFromRecents="true"</span> +<span class="d">- android:launchMode="singleTask"</span> +<span class="d">- android:stateNotNeeded="true"</span> +<span class="d">- android:taskAffinity="dev.equwal.assistkey.home"</span> +<span class="d">- android:windowSoftInputMode="stateHidden|adjustResize"></span> +<span class="d">- <intent-filter></span> +<span class="d">- <action android:name="android.intent.action.MAIN" /></span> +<span class="d">- <category android:name="android.intent.category.HOME" /></span> +<span class="d">- <category android:name="android.intent.category.DEFAULT" /></span> +<span class="d">- </intent-filter></span> +<span class="d">- </activity></span> + <activity android:name=".ui.ReportActivity" android:exported="false" /> + <activity android:name=".ui.DetectActivity" android:exported="false" /> + +diff --git a/app/src/main/java/dev/equwal/assistkey/channel/Channel.kt b/app/src/main/java/dev/equwal/assistkey/channel/Channel.kt +index 06326c2..8714ee9 100644 +--- a/app/src/main/java/dev/equwal/assistkey/channel/Channel.kt ++++ b/app/src/main/java/dev/equwal/assistkey/channel/Channel.kt +<span class="h">@@ -32,22 +32,22 @@ enum class Channel(</span> + + ASSISTANT( + "assistant", +<span class="d">- "Digital assistant",</span> +<span class="d">- "Catches a held Power button",</span> +<span class="a">+ "Hold Power",</span> +<span class="a">+ "Through the assistant role",</span> + listOf("dev.equwal.assistkey.channel.AssistActivity") + ), + + CAMERA( + "camera", +<span class="d">- "Camera app",</span> +<span class="d">- "Catches a double press of Power",</span> +<span class="a">+ "Double tap Power, by camera",</span> +<span class="a">+ "Most devices send it to the camera",</span> + listOf("dev.equwal.assistkey.channel.CameraShimActivity") + ), + + WALLET( + "wallet", +<span class="d">- "Wallet app",</span> +<span class="d">- "Catches the wallet tile and button",</span> +<span class="a">+ "Double tap Power, by wallet",</span> +<span class="a">+ "Some devices send it to the wallet",</span> + listOf( + "dev.equwal.assistkey.channel.WalletActivity", + "dev.equwal.assistkey.channel.WalletService" +diff --git a/app/src/main/java/dev/equwal/assistkey/home/Apps.kt b/app/src/main/java/dev/equwal/assistkey/home/Apps.kt +deleted file mode 100644 +index 3ccaa35..0000000 +--- a/app/src/main/java/dev/equwal/assistkey/home/Apps.kt ++++ /dev/null +<span class="h">@@ -1,173 +0,0 @@</span> +<span class="d">-package dev.equwal.assistkey.home</span> +<span class="d">-</span> +<span class="d">-import android.app.usage.UsageEvents</span> +<span class="d">-import android.app.usage.UsageStatsManager</span> +<span class="d">-import android.content.ComponentName</span> +<span class="d">-import android.content.Context</span> +<span class="d">-import android.content.Intent</span> +<span class="d">-import android.content.pm.PackageManager</span> +<span class="d">-import android.os.Process</span> +<span class="d">-import dev.equwal.assistkey.shell.Shell</span> +<span class="d">-</span> +<span class="d">-/** The apps on this device, as the home screen and the recents list need them. */</span> +<span class="d">-object Apps {</span> +<span class="d">-</span> +<span class="d">- data class App(val label: String, val pkg: String, val activity: String) {</span> +<span class="d">- val component: ComponentName get() = ComponentName(pkg, activity)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private const val PREFS = "assistkey_home"</span> +<span class="d">- private const val K_FAVS = "favourites" // ordered, newline separated packages</span> +<span class="d">- private const val K_HIDDEN = "hidden"</span> +<span class="d">- private const val K_CLOCK = "clock"</span> +<span class="d">- private const val K_AUTO = "auto_open"</span> +<span class="d">-</span> +<span class="d">- private fun prefs(c: Context) = c.getSharedPreferences(PREFS, Context.MODE_PRIVATE)</span> +<span class="d">-</span> +<span class="d">- /** Everything with a launcher icon, alphabetical, ourselves included. */</span> +<span class="d">- fun all(c: Context): List<App> {</span> +<span class="d">- val pm = c.packageManager</span> +<span class="d">- val main = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)</span> +<span class="d">- return pm.queryIntentActivities(main, 0)</span> +<span class="d">- .map { App(it.loadLabel(pm).toString(), it.activityInfo.packageName, it.activityInfo.name) }</span> +<span class="d">- .distinctBy { it.pkg + "/" + it.activity }</span> +<span class="d">- .sortedBy { it.label.lowercase() }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- fun visible(c: Context): List<App> = hidden(c).let { h -> all(c).filter { it.pkg !in h } }</span> +<span class="d">-</span> +<span class="d">- fun launch(c: Context, app: App): Boolean = runCatching {</span> +<span class="d">- c.startActivity(</span> +<span class="d">- Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)</span> +<span class="d">- .setComponent(app.component)</span> +<span class="d">- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)</span> +<span class="d">- )</span> +<span class="d">- true</span> +<span class="d">- }.getOrDefault(false)</span> +<span class="d">-</span> +<span class="d">- // ---- preferences -------------------------------------------------------------------------</span> +<span class="d">-</span> +<span class="d">- fun favourites(c: Context): List<String> =</span> +<span class="d">- prefs(c).getString(K_FAVS, "").orEmpty().lines().filter { it.isNotBlank() }</span> +<span class="d">-</span> +<span class="d">- fun setFavourites(c: Context, pkgs: List<String>) {</span> +<span class="d">- prefs(c).edit().putString(K_FAVS, pkgs.distinct().joinToString("\n")).apply()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- fun toggleFavourite(c: Context, pkg: String) {</span> +<span class="d">- val f = favourites(c)</span> +<span class="d">- setFavourites(c, if (pkg in f) f - pkg else f + pkg)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- fun hidden(c: Context): Set<String> = prefs(c).getStringSet(K_HIDDEN, emptySet()).orEmpty()</span> +<span class="d">-</span> +<span class="d">- fun toggleHidden(c: Context, pkg: String) {</span> +<span class="d">- val h = hidden(c)</span> +<span class="d">- prefs(c).edit().putStringSet(K_HIDDEN, if (pkg in h) h - pkg else h + pkg).apply()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- fun showClock(c: Context): Boolean = prefs(c).getBoolean(K_CLOCK, true)</span> +<span class="d">- fun setShowClock(c: Context, on: Boolean) = prefs(c).edit().putBoolean(K_CLOCK, on).apply()</span> +<span class="d">-</span> +<span class="d">- /** Open the app by itself once typing has narrowed the list to one. */</span> +<span class="d">- fun autoOpen(c: Context): Boolean = prefs(c).getBoolean(K_AUTO, true)</span> +<span class="d">- fun setAutoOpen(c: Context, on: Boolean) = prefs(c).edit().putBoolean(K_AUTO, on).apply()</span> +<span class="d">-</span> +<span class="d">- // ---- recents -----------------------------------------------------------------------------</span> +<span class="d">-</span> +<span class="d">- fun hasUsageAccess(c: Context): Boolean {</span> +<span class="d">- val ops = c.getSystemService(android.app.AppOpsManager::class.java) ?: return false</span> +<span class="d">- return ops.unsafeCheckOpNoThrow(</span> +<span class="d">- android.app.AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), c.packageName</span> +<span class="d">- ) == android.app.AppOpsManager.MODE_ALLOWED</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /** Most recent first, from the usage log. Needs the user's usage-access grant. */</span> +<span class="d">- fun recentFromUsage(c: Context, limit: Int = 12): List<App> {</span> +<span class="d">- val usm = c.getSystemService(UsageStatsManager::class.java) ?: return emptyList()</span> +<span class="d">- val now = System.currentTimeMillis()</span> +<span class="d">- val events = usm.queryEvents(now - 3L * 24 * 60 * 60 * 1000, now)</span> +<span class="d">- val last = LinkedHashMap<String, Long>()</span> +<span class="d">- val e = UsageEvents.Event()</span> +<span class="d">- while (events.hasNextEvent()) {</span> +<span class="d">- events.getNextEvent(e)</span> +<span class="d">- if (e.eventType == UsageEvents.Event.ACTIVITY_RESUMED) last[e.packageName] = e.timeStamp</span> +<span class="d">- }</span> +<span class="d">- return rank(c, open(last, closedAt(c)), limit)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /**</span> +<span class="d">- * The packages to show, most recent first. An app the user closed stays</span> +<span class="d">- * off the list until the user opens that app again.</span> +<span class="d">- */</span> +<span class="d">- fun open(lastUsed: Map<String, Long>, closedAt: Map<String, Long>): List<String> =</span> +<span class="d">- lastUsed.entries</span> +<span class="d">- .filter { (pkg, at) -> at > (closedAt[pkg] ?: Long.MIN_VALUE) }</span> +<span class="d">- .sortedByDescending { it.value }</span> +<span class="d">- .map { it.key }</span> +<span class="d">-</span> +<span class="d">- private const val RECENTS_PREFS = "assistkey_recents"</span> +<span class="d">-</span> +<span class="d">- private fun closedAt(c: Context): Map<String, Long> =</span> +<span class="d">- c.getSharedPreferences(RECENTS_PREFS, Context.MODE_PRIVATE).all</span> +<span class="d">- .mapNotNull { (pkg, at) -> (at as? Long)?.let { pkg to it } }.toMap()</span> +<span class="d">-</span> +<span class="d">- /**</span> +<span class="d">- * Closes [pkg] without shell access: Android ends the background processes</span> +<span class="d">- * of the app, and the app leaves the list of this screen.</span> +<span class="d">- */</span> +<span class="d">- fun closeWithoutShell(c: Context, pkg: String) {</span> +<span class="d">- if (pkg == c.packageName) return</span> +<span class="d">- runCatching { c.getSystemService(android.app.ActivityManager::class.java)?.killBackgroundProcesses(pkg) }</span> +<span class="d">- c.getSharedPreferences(RECENTS_PREFS, Context.MODE_PRIVATE).edit()</span> +<span class="d">- .putLong(pkg, System.currentTimeMillis()).apply()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /** One entry of the recent-apps list. [taskId] is known only with shell access. */</span> +<span class="d">- data class Recent(val app: App, val taskId: Int?)</span> +<span class="d">-</span> +<span class="d">- /**</span> +<span class="d">- * Most recent first, from the real task list of the system. Needs shell</span> +<span class="d">- * access. A task line names its app as `A=<uid>:<package>` or, for some</span> +<span class="d">- * tasks, as `I=<package>/<class>`.</span> +<span class="d">- */</span> +<span class="d">- fun recentFromShell(c: Context, limit: Int = 12, done: (List<Recent>?) -> Unit) {</span> +<span class="d">- if (!Shell.ready) return done(null)</span> +<span class="d">- Shell.run("dumpsys activity recents") { r -></span> +<span class="d">- if (!r.ok) return@run done(null)</span> +<span class="d">- val task = Regex("""Recent #\d+: Task\{\S+ #(\d+) type=(\w+) (?:A=\d+:|I=)([^\s/}]+)""")</span> +<span class="d">- val ids = LinkedHashMap<String, Int>()</span> +<span class="d">- task.findAll(r.output)</span> +<span class="d">- .filter { it.groupValues[2] == "standard" }</span> +<span class="d">- .forEach { ids.putIfAbsent(it.groupValues[3], it.groupValues[1].toInt()) }</span> +<span class="d">- done(rank(c, ids.keys.toList(), limit).map { Recent(it, ids[it.pkg]) })</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /**</span> +<span class="d">- * The picture the system keeps of a task. The files belong to `system`, so</span> +<span class="d">- * this works only where the shell is root, or may use `su`. Null otherwise.</span> +<span class="d">- */</span> +<span class="d">- fun snapshot(taskId: Int, done: (android.graphics.Bitmap?) -> Unit) {</span> +<span class="d">- val dir = "/data/system_ce/0/snapshots/"</span> +<span class="d">- val pick = "f=$dir${taskId}_reduced.jpg; [ -e \$f ] || f=$dir$taskId.jpg; "</span> +<span class="d">- Shell.run(pick + "base64 -w 0 \$f 2>/dev/null || su 0 base64 -w 0 \$f 2>/dev/null") { r -></span> +<span class="d">- val bytes = runCatching { android.util.Base64.decode(r.output.trim(), android.util.Base64.DEFAULT) }.getOrNull()</span> +<span class="d">- done(bytes?.takeIf { it.size > 100 }?.let { android.graphics.BitmapFactory.decodeByteArray(it, 0, it.size) })</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun rank(c: Context, pkgs: List<String>, limit: Int): List<App> {</span> +<span class="d">- val byPkg = all(c).associateBy { it.pkg }</span> +<span class="d">- val home = homePackages(c)</span> +<span class="d">- return pkgs.distinct()</span> +<span class="d">- .filter { it != c.packageName && it !in home }</span> +<span class="d">- .mapNotNull { byPkg[it] }</span> +<span class="d">- .take(limit)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun homePackages(c: Context): Set<String> =</span> +<span class="d">- c.packageManager.queryIntentActivities(</span> +<span class="d">- Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), PackageManager.MATCH_DEFAULT_ONLY</span> +<span class="d">- ).map { it.activityInfo.packageName }.toSet()</span> +<span class="d">-}</span> +diff --git a/app/src/main/java/dev/equwal/assistkey/home/HomeActivity.kt b/app/src/main/java/dev/equwal/assistkey/home/HomeActivity.kt +deleted file mode 100644 +index 7f6b8d7..0000000 +--- a/app/src/main/java/dev/equwal/assistkey/home/HomeActivity.kt ++++ /dev/null +<span class="h">@@ -1,209 +0,0 @@</span> +<span class="d">-package dev.equwal.assistkey.home</span> +<span class="d">-</span> +<span class="d">-import android.app.Activity</span> +<span class="d">-import android.app.AlertDialog</span> +<span class="d">-import android.content.Intent</span> +<span class="d">-import android.graphics.Color</span> +<span class="d">-import android.graphics.Typeface</span> +<span class="d">-import android.net.Uri</span> +<span class="d">-import android.os.Bundle</span> +<span class="d">-import android.provider.Settings</span> +<span class="d">-import android.text.Editable</span> +<span class="d">-import android.text.InputType</span> +<span class="d">-import android.text.TextWatcher</span> +<span class="d">-import android.util.TypedValue</span> +<span class="d">-import android.view.Gravity</span> +<span class="d">-import android.view.ViewGroup.LayoutParams.MATCH_PARENT</span> +<span class="d">-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT</span> +<span class="d">-import android.view.WindowInsets</span> +<span class="d">-import android.view.inputmethod.EditorInfo</span> +<span class="d">-import android.view.inputmethod.InputMethodManager</span> +<span class="d">-import android.widget.EditText</span> +<span class="d">-import android.widget.LinearLayout</span> +<span class="d">-import android.widget.ScrollView</span> +<span class="d">-import android.widget.TextClock</span> +<span class="d">-import android.widget.TextView</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.dp</span> +<span class="d">-</span> +<span class="d">-/**</span> +<span class="d">- * A home screen with nothing on it.</span> +<span class="d">- *</span> +<span class="d">- * A clock, the handful of apps you chose, and a line to type in. The full app</span> +<span class="d">- * list is never shown: type a few letters and the matches appear, and when only</span> +<span class="d">- * one is left it opens by itself. On e-ink that is also the fastest design</span> +<span class="d">- * there is - no icons to draw, no grid to scroll, nothing that animates.</span> +<span class="d">- *</span> +<span class="d">- * Original code. The idea of a launcher that hides everything until you type is</span> +<span class="d">- * shared with several open-source launchers; none of their code is here.</span> +<span class="d">- */</span> +<span class="d">-class HomeActivity : Activity() {</span> +<span class="d">-</span> +<span class="d">- private lateinit var results: LinearLayout</span> +<span class="d">- private lateinit var favourites: LinearLayout</span> +<span class="d">- private lateinit var search: EditText</span> +<span class="d">- private var apps: List<Apps.App> = emptyList()</span> +<span class="d">-</span> +<span class="d">- override fun onCreate(savedInstanceState: Bundle?) {</span> +<span class="d">- super.onCreate(savedInstanceState)</span> +<span class="d">- build()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- override fun onResume() {</span> +<span class="d">- super.onResume()</span> +<span class="d">- apps = Apps.visible(this)</span> +<span class="d">- search.setText("")</span> +<span class="d">- showFavourites()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /** Home is the bottom of the stack. Back has nowhere to go. */</span> +<span class="d">- @Deprecated("Deprecated in Java")</span> +<span class="d">- override fun onBackPressed() {</span> +<span class="d">- if (search.text.isNotEmpty()) search.setText("")</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- override fun onNewIntent(intent: Intent?) {</span> +<span class="d">- super.onNewIntent(intent)</span> +<span class="d">- // Home pressed while already home: clear the search.</span> +<span class="d">- if (::search.isInitialized) search.setText("")</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- // ---- layout ---------------------------------------------------------------------------------</span> +<span class="d">-</span> +<span class="d">- private fun build() {</span> +<span class="d">- val col = LinearLayout(this).apply {</span> +<span class="d">- orientation = LinearLayout.VERTICAL</span> +<span class="d">- val p = dp(28)</span> +<span class="d">- setPadding(p, dp(36), p, p)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- if (Apps.showClock(this)) {</span> +<span class="d">- col.addView(TextClock(this).apply {</span> +<span class="d">- format12Hour = "h:mm"</span> +<span class="d">- format24Hour = "H:mm"</span> +<span class="d">- setTextColor(Color.BLACK)</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 64f)</span> +<span class="d">- typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)</span> +<span class="d">- setOnClickListener { open(Intent(android.provider.AlarmClock.ACTION_SHOW_ALARMS)) }</span> +<span class="d">- })</span> +<span class="d">- col.addView(TextClock(this).apply {</span> +<span class="d">- format12Hour = "EEEE, d MMMM"</span> +<span class="d">- format24Hour = "EEEE, d MMMM"</span> +<span class="d">- setTextColor(Color.rgb(70, 70, 70))</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)</span> +<span class="d">- setPadding(0, 0, 0, dp(28))</span> +<span class="d">- })</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- favourites = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }</span> +<span class="d">- col.addView(favourites)</span> +<span class="d">-</span> +<span class="d">- search = EditText(this).apply {</span> +<span class="d">- hint = "Type to find an app"</span> +<span class="d">- setSingleLine()</span> +<span class="d">- inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS</span> +<span class="d">- imeOptions = EditorInfo.IME_ACTION_GO</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)</span> +<span class="d">- setTextColor(Color.BLACK)</span> +<span class="d">- setPadding(0, dp(20), 0, dp(12))</span> +<span class="d">- addTextChangedListener(object : TextWatcher {</span> +<span class="d">- override fun afterTextChanged(s: Editable?) = filter(s?.toString().orEmpty())</span> +<span class="d">- override fun beforeTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) = Unit</span> +<span class="d">- override fun onTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) = Unit</span> +<span class="d">- })</span> +<span class="d">- setOnEditorActionListener { _, _, _ -> matches(text.toString()).firstOrNull()?.let(::launch); true }</span> +<span class="d">- }</span> +<span class="d">- col.addView(search, LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT))</span> +<span class="d">-</span> +<span class="d">- results = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }</span> +<span class="d">- col.addView(results)</span> +<span class="d">-</span> +<span class="d">- val scroll = ScrollView(this).apply {</span> +<span class="d">- setBackgroundColor(Color.WHITE)</span> +<span class="d">- isFillViewport = true</span> +<span class="d">- clipToPadding = false</span> +<span class="d">- addView(col, LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT))</span> +<span class="d">- setOnApplyWindowInsetsListener { v, insets -></span> +<span class="d">- val bars = insets.getInsets(WindowInsets.Type.systemBars() or WindowInsets.Type.ime())</span> +<span class="d">- v.setPadding(bars.left, bars.top, bars.right, bars.bottom)</span> +<span class="d">- insets</span> +<span class="d">- }</span> +<span class="d">- // Long-press on empty space is the only way into settings: there is</span> +<span class="d">- // deliberately no button for it.</span> +<span class="d">- setOnLongClickListener { startActivity(Intent(this@HomeActivity, HomeSettingsActivity::class.java)); true }</span> +<span class="d">- }</span> +<span class="d">- col.setOnLongClickListener { startActivity(Intent(this, HomeSettingsActivity::class.java)); true }</span> +<span class="d">- setContentView(scroll)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun line(app: Apps.App, size: Float): TextView = TextView(this).apply {</span> +<span class="d">- text = app.label</span> +<span class="d">- setTextColor(Color.BLACK)</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, size)</span> +<span class="d">- gravity = Gravity.START</span> +<span class="d">- setPadding(0, dp(12), 0, dp(12))</span> +<span class="d">- setOnClickListener { launch(app) }</span> +<span class="d">- setOnLongClickListener { menu(app); true }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- // ---- behaviour -----------------------------------------------------------------------------</span> +<span class="d">-</span> +<span class="d">- private fun showFavourites() {</span> +<span class="d">- favourites.removeAllViews()</span> +<span class="d">- val byPkg = apps.associateBy { it.pkg }</span> +<span class="d">- val favs = Apps.favourites(this).mapNotNull { byPkg[it] }</span> +<span class="d">- favs.forEach { favourites.addView(line(it, 26f)) }</span> +<span class="d">- if (favs.isEmpty()) {</span> +<span class="d">- favourites.addView(TextView(this).apply {</span> +<span class="d">- text = "Find an app below, then press and hold it to keep it here."</span> +<span class="d">- setTextColor(Color.rgb(90, 90, 90))</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)</span> +<span class="d">- })</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun matches(q: String): List<Apps.App> {</span> +<span class="d">- val needle = q.trim().lowercase()</span> +<span class="d">- if (needle.isEmpty()) return emptyList()</span> +<span class="d">- // Starts-with first, then anywhere in the name, then initials ("gm" finds Google Maps).</span> +<span class="d">- val starts = apps.filter { it.label.lowercase().startsWith(needle) }</span> +<span class="d">- val inside = apps.filter { needle in it.label.lowercase() } - starts.toSet()</span> +<span class="d">- val initials = apps.filter { a -></span> +<span class="d">- a.label.split(' ', '-', '_').mapNotNull { it.firstOrNull()?.lowercaseChar() }.joinToString("").startsWith(needle)</span> +<span class="d">- } - starts.toSet() - inside.toSet()</span> +<span class="d">- return starts + inside + initials</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun filter(q: String) {</span> +<span class="d">- results.removeAllViews()</span> +<span class="d">- val found = matches(q)</span> +<span class="d">- if (found.size == 1 && q.trim().length >= 2 && Apps.autoOpen(this)) {</span> +<span class="d">- launch(found[0])</span> +<span class="d">- return</span> +<span class="d">- }</span> +<span class="d">- found.take(12).forEach { results.addView(line(it, 22f)) }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun launch(app: Apps.App) {</span> +<span class="d">- getSystemService(InputMethodManager::class.java)?.hideSoftInputFromWindow(search.windowToken, 0)</span> +<span class="d">- Apps.launch(this, app)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun open(i: Intent) {</span> +<span class="d">- runCatching { startActivity(i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun menu(app: Apps.App) {</span> +<span class="d">- val fav = app.pkg in Apps.favourites(this)</span> +<span class="d">- val items = arrayOf(if (fav) "Remove from home" else "Keep on home", "Hide from search", "App info")</span> +<span class="d">- AlertDialog.Builder(this).setTitle(app.label).setItems(items) { _, which -></span> +<span class="d">- when (which) {</span> +<span class="d">- 0 -> Apps.toggleFavourite(this, app.pkg)</span> +<span class="d">- 1 -> Apps.toggleHidden(this, app.pkg)</span> +<span class="d">- 2 -> open(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + app.pkg)))</span> +<span class="d">- }</span> +<span class="d">- apps = Apps.visible(this)</span> +<span class="d">- search.setText("")</span> +<span class="d">- showFavourites()</span> +<span class="d">- }.show()</span> +<span class="d">- }</span> +<span class="d">-}</span> +diff --git a/app/src/main/java/dev/equwal/assistkey/home/HomeSettingsActivity.kt b/app/src/main/java/dev/equwal/assistkey/home/HomeSettingsActivity.kt +deleted file mode 100644 +index fbecb26..0000000 +--- a/app/src/main/java/dev/equwal/assistkey/home/HomeSettingsActivity.kt ++++ /dev/null +<span class="h">@@ -1,107 +0,0 @@</span> +<span class="d">-package dev.equwal.assistkey.home</span> +<span class="d">-</span> +<span class="d">-import android.app.Activity</span> +<span class="d">-import android.app.role.RoleManager</span> +<span class="d">-import android.content.ComponentName</span> +<span class="d">-import android.content.Intent</span> +<span class="d">-import android.content.pm.PackageManager</span> +<span class="d">-import android.provider.Settings</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.button</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.check</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.header</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.note</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.primaryButton</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.row</span> +<span class="d">-</span> +<span class="d">-/**</span> +<span class="d">- * Switching the AssistKey home screen on, and tuning it.</span> +<span class="d">- *</span> +<span class="d">- * The home activity is disabled in the manifest. Until the user asks for it</span> +<span class="d">- * here the app is not a launcher and never shows up as one.</span> +<span class="d">- */</span> +<span class="d">-class HomeSettingsActivity : Activity() {</span> +<span class="d">-</span> +<span class="d">- private val home by lazy { ComponentName(this, HomeActivity::class.java) }</span> +<span class="d">-</span> +<span class="d">- override fun onResume() {</span> +<span class="d">- super.onResume()</span> +<span class="d">- build()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun enabled(): Boolean =</span> +<span class="d">- packageManager.getComponentEnabledSetting(home) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED</span> +<span class="d">-</span> +<span class="d">- private fun isDefault(): Boolean {</span> +<span class="d">- val res = packageManager.resolveActivity(</span> +<span class="d">- Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), PackageManager.MATCH_DEFAULT_ONLY</span> +<span class="d">- )</span> +<span class="d">- return res?.activityInfo?.packageName == packageName</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun build() {</span> +<span class="d">- val col = Ui.page(this, "Home screen")</span> +<span class="d">-</span> +<span class="d">- col.check("Offer Rebind as a home screen", null, enabled()) { on -></span> +<span class="d">- packageManager.setComponentEnabledSetting(</span> +<span class="d">- home,</span> +<span class="d">- if (on) PackageManager.COMPONENT_ENABLED_STATE_ENABLED</span> +<span class="d">- else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,</span> +<span class="d">- PackageManager.DONT_KILL_APP</span> +<span class="d">- )</span> +<span class="d">- build()</span> +<span class="d">- }</span> +<span class="d">- if (!enabled()) return</span> +<span class="d">-</span> +<span class="d">- col.row(</span> +<span class="d">- "Home screen",</span> +<span class="d">- null,</span> +<span class="d">- enabled = false,</span> +<span class="d">- state = if (isDefault()) "In use" else "Ready"</span> +<span class="d">- )</span> +<span class="d">- if (!isDefault()) {</span> +<span class="d">- col.primaryButton("Make it the home screen") {</span> +<span class="d">- val rm = getSystemService(RoleManager::class.java)</span> +<span class="d">- val i = if (rm != null && rm.isRoleAvailable(RoleManager.ROLE_HOME)) {</span> +<span class="d">- rm.createRequestRoleIntent(RoleManager.ROLE_HOME)</span> +<span class="d">- } else Intent(Settings.ACTION_HOME_SETTINGS)</span> +<span class="d">- runCatching { startActivityForResult(i, 1) }</span> +<span class="d">- .onFailure { runCatching { startActivity(Intent(Settings.ACTION_HOME_SETTINGS)) } }</span> +<span class="d">- }</span> +<span class="d">- } else {</span> +<span class="d">- col.button("Choose a different home screen") {</span> +<span class="d">- runCatching { startActivity(Intent(Settings.ACTION_HOME_SETTINGS)) }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- col.button("Open it") { startActivity(Intent(this, HomeActivity::class.java)) }</span> +<span class="d">-</span> +<span class="d">- col.header("Options")</span> +<span class="d">- col.check("Clock and date", null, Apps.showClock(this)) { Apps.setShowClock(this, it) }</span> +<span class="d">- col.check("Open the app when one match is left", "After at least two letters", Apps.autoOpen(this)) {</span> +<span class="d">- Apps.setAutoOpen(this, it)</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- col.header("On the home screen")</span> +<span class="d">- val byPkg = Apps.all(this).associateBy { it.pkg }</span> +<span class="d">- val favs = Apps.favourites(this).mapNotNull { byPkg[it] }</span> +<span class="d">- if (favs.isEmpty()) col.note("Nothing yet.")</span> +<span class="d">- favs.forEach { app -></span> +<span class="d">- col.row(app.label, "Tap to remove") { Apps.toggleFavourite(this, app.pkg); build() }</span> +<span class="d">- }</span> +<span class="d">- col.button("Add an app") {</span> +<span class="d">- val all = Apps.all(this).filter { it.pkg !in Apps.favourites(this) }</span> +<span class="d">- Ui.pick(this, "Keep on home", all.map { it.label }) { i -></span> +<span class="d">- Apps.toggleFavourite(this, all[i].pkg); build()</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- val hidden = Apps.hidden(this).mapNotNull { byPkg[it] }</span> +<span class="d">- if (hidden.isNotEmpty()) {</span> +<span class="d">- col.header("Hidden from search")</span> +<span class="d">- hidden.forEach { app -></span> +<span class="d">- col.row(app.label, "Tap to show again") { Apps.toggleHidden(this, app.pkg); build() }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- col.note("Press and hold empty space to come back here.")</span> +<span class="d">- }</span> +<span class="d">-}</span> +diff --git a/app/src/main/java/dev/equwal/assistkey/home/RecentsActivity.kt b/app/src/main/java/dev/equwal/assistkey/home/RecentsActivity.kt +index 1be789a..9f8a50b 100644 +--- a/app/src/main/java/dev/equwal/assistkey/home/RecentsActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/home/RecentsActivity.kt +<span class="h">@@ -2,375 +2,31 @@ package dev.equwal.assistkey.home</span> + + import android.app.Activity + import android.content.Intent +<span class="d">-import android.graphics.Color</span> +<span class="d">-import android.graphics.Typeface</span> +<span class="d">-import android.graphics.drawable.GradientDrawable</span> +<span class="d">-import android.provider.Settings</span> +<span class="d">-import android.util.TypedValue</span> +<span class="d">-import android.view.GestureDetector</span> +<span class="d">-import android.view.Gravity</span> +<span class="d">-import android.view.MotionEvent</span> +<span class="d">-import android.view.View</span> +<span class="d">-import android.view.ViewGroup.LayoutParams.MATCH_PARENT</span> +<span class="d">-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT</span> +<span class="d">-import android.view.WindowInsets</span> +<span class="d">-import android.widget.HorizontalScrollView</span> +<span class="d">-import android.widget.ImageView</span> +<span class="d">-import android.widget.LinearLayout</span> +<span class="d">-import android.widget.TextView</span> +<span class="d">-import dev.equwal.assistkey.shell.Shell</span> +<span class="d">-import dev.equwal.assistkey.ui.ShellActivity</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.button</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.dp</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.note</span> +<span class="d">-import dev.equwal.assistkey.ui.Ui.title</span> +<span class="d">-import kotlin.math.abs</span> +<span class="a">+import android.os.Bundle</span> +<span class="a">+import android.widget.Toast</span> +<span class="a">+import dev.equwal.assistkey.ui.HomeHubActivity</span> + + /** +<span class="d">- * Recent apps, as a row of cards.</span> +<span class="a">+ * The "Recent apps" action. The recent-apps screen is a program of its own,</span> +<span class="a">+ * Ink Recents. This activity has no screen: it opens Ink Recents and ends.</span> +<span class="a">+ * Where Ink Recents is not installed, it opens the screen that offers it.</span> + * +<span class="d">- * It works like the Android switcher: swipe sideways through the cards, tap a</span> +<span class="d">- * card to go to that app, swipe a card up to close it. It is drawn for e-ink:</span> +<span class="d">- * black outlines on white, and the row jumps from card to card with no</span> +<span class="d">- * animation.</span> +<span class="d">- *</span> +<span class="d">- * Android does not tell apps what is running. With shell access the cards are</span> +<span class="d">- * the real tasks of the system, a card can be closed, and where the shell is</span> +<span class="d">- * root each card shows the picture the system keeps of the task. Without shell</span> +<span class="d">- * access the order comes from the usage log and a card shows the app icon.</span> +<span class="d">- *</span> +<span class="d">- * Bind it to a key from any action list: AssistKey > Recent apps.</span> +<span class="a">+ * The class keeps its old name, because bindings made before hold the name.</span> + */ + class RecentsActivity : Activity() { + +<span class="d">- private var row: LinearLayout? = null</span> +<span class="d">- private var scroller: HorizontalScrollView? = null</span> +<span class="d">- private var strip: LinearLayout? = null</span> +<span class="d">- private var heading: TextView? = null</span> +<span class="d">- private var closeOthers: TextView? = null</span> +<span class="d">- private var names: List<String> = emptyList()</span> +<span class="d">- private var shown: List<Apps.Recent> = emptyList()</span> +<span class="d">- private var cardStep = 0</span> +<span class="d">- private var index = 0</span> +<span class="d">-</span> +<span class="d">- /** Each time the screen opens, it starts at the newest app and shows the hint again. */</span> +<span class="d">- override fun onStart() {</span> +<span class="d">- super.onStart()</span> +<span class="d">- index = 0</span> +<span class="d">- hinted = false</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- override fun onResume() {</span> +<span class="d">- super.onResume()</span> +<span class="d">- load()</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun load() {</span> +<span class="d">- Apps.recentFromShell(this) { fromShell -></span> +<span class="d">- if (isFinishing) return@recentFromShell</span> +<span class="d">- when {</span> +<span class="d">- fromShell != null -> show(fromShell, closable = true)</span> +<span class="d">- Apps.hasUsageAccess(this) -></span> +<span class="d">- show(Apps.recentFromUsage(this).map { Apps.Recent(it, null) }, closable = false)</span> +<span class="d">- else -> askForAccess()</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- // ---- the cards -------------------------------------------------------------------------</span> +<span class="d">-</span> +<span class="d">- private fun show(recents: List<Apps.Recent>, closable: Boolean) {</span> +<span class="d">- val screenW = resources.displayMetrics.widthPixels</span> +<span class="d">- val screenH = resources.displayMetrics.heightPixels</span> +<span class="d">- val cardW = (screenW * 0.70f).toInt()</span> +<span class="d">- val cardH = (screenH * 0.56f).toInt()</span> +<span class="d">- val gap = dp(16)</span> +<span class="d">- cardStep = cardW + gap</span> +<span class="d">- names = recents.map { it.app.label }</span> +<span class="d">- shown = recents</span> +<span class="d">- index = index.coerceIn(0, maxOf(0, recents.size - 1))</span> +<span class="d">-</span> +<span class="d">- val root = LinearLayout(this).apply {</span> +<span class="d">- orientation = LinearLayout.VERTICAL</span> +<span class="d">- setBackgroundColor(Color.WHITE)</span> +<span class="d">- setOnApplyWindowInsetsListener { v, insets -></span> +<span class="d">- val bars = insets.getInsets(WindowInsets.Type.systemBars())</span> +<span class="d">- v.setPadding(bars.left, bars.top, bars.right, bars.bottom)</span> +<span class="d">- insets</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- heading = TextView(this).apply {</span> +<span class="d">- text = if (recents.isEmpty()) "Nothing recent" else "Recent apps"</span> +<span class="d">- setTextColor(Color.BLACK)</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f)</span> +<span class="d">- typeface = Typeface.DEFAULT_BOLD</span> +<span class="d">- setSingleLine()</span> +<span class="d">- setPadding(dp(20), dp(16), dp(20), dp(12))</span> +<span class="d">- }</span> +<span class="d">- root.addView(heading)</span> +<span class="d">-</span> +<span class="d">- val cards = LinearLayout(this).apply {</span> +<span class="d">- orientation = LinearLayout.HORIZONTAL</span> +<span class="d">- // Side padding puts the first and the last card in the middle too.</span> +<span class="d">- val side = (screenW - cardW) / 2</span> +<span class="d">- setPadding(side, 0, side, 0)</span> +<span class="d">- }</span> +<span class="d">- recents.forEach { r -></span> +<span class="d">- cards.addView(</span> +<span class="d">- card(r, closable),</span> +<span class="d">- LinearLayout.LayoutParams(cardW, cardH).apply { marginEnd = gap }</span> +<span class="d">- )</span> +<span class="d">- }</span> +<span class="d">- row = cards</span> +<span class="d">-</span> +<span class="d">- val scroll = object : HorizontalScrollView(this) {</span> +<span class="d">- private val slop = android.view.ViewConfiguration.get(context).scaledTouchSlop</span> +<span class="d">- private var downX = 0f</span> +<span class="d">- private var downY = 0f</span> +<span class="d">-</span> +<span class="d">- /** A sideways drag belongs to the row. A tap or an upward swipe belongs to the card. */</span> +<span class="d">- override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {</span> +<span class="d">- when (ev.actionMasked) {</span> +<span class="d">- MotionEvent.ACTION_DOWN -> { downX = ev.x; downY = ev.y }</span> +<span class="d">- MotionEvent.ACTION_MOVE -> {</span> +<span class="d">- val dx = abs(ev.x - downX)</span> +<span class="d">- if (dx > slop && dx > abs(ev.y - downY)) return true</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- return false</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /**</span> +<span class="d">- * One swipe, one card, in one step. The row does not follow the</span> +<span class="d">- * finger: e-ink shows every frame of a moving row as a smear.</span> +<span class="d">- */</span> +<span class="d">- override fun onTouchEvent(ev: MotionEvent): Boolean {</span> +<span class="d">- if (ev.actionMasked == MotionEvent.ACTION_UP) {</span> +<span class="d">- val dx = ev.x - downX</span> +<span class="d">- if (abs(dx) > slop) show(index + if (dx < 0) 1 else -1)</span> +<span class="d">- }</span> +<span class="d">- return true</span> +<span class="d">- }</span> +<span class="d">- }.apply {</span> +<span class="d">- isHorizontalScrollBarEnabled = false</span> +<span class="d">- overScrollMode = View.OVER_SCROLL_NEVER</span> +<span class="d">- addView(cards, LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT))</span> +<span class="d">- }</span> +<span class="d">- scroller = scroll</span> +<span class="d">- root.addView(scroll, LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT))</span> +<span class="d">-</span> +<span class="d">- // Every recent app at once, as icons. The one in the middle is marked,</span> +<span class="d">- // and a tap on an icon brings its card to the middle.</span> +<span class="d">- val icons = LinearLayout(this).apply {</span> +<span class="d">- orientation = LinearLayout.HORIZONTAL</span> +<span class="d">- gravity = Gravity.CENTER_HORIZONTAL</span> +<span class="d">- setPadding(dp(12), dp(14), dp(12), dp(4))</span> +<span class="d">- }</span> +<span class="d">- recents.forEachIndexed { i, r -></span> +<span class="d">- icons.addView(</span> +<span class="d">- ImageView(this).apply {</span> +<span class="d">- setImageDrawable(runCatching { packageManager.getApplicationIcon(r.app.pkg) }.getOrNull())</span> +<span class="d">- contentDescription = r.app.label</span> +<span class="d">- val p = dp(6)</span> +<span class="d">- setPadding(p, p, p, p)</span> +<span class="d">- setOnClickListener { show(i) }</span> +<span class="d">- },</span> +<span class="d">- LinearLayout.LayoutParams(dp(52), dp(52)).apply { marginEnd = dp(4) }</span> +<span class="d">- )</span> +<span class="d">- }</span> +<span class="d">- strip = icons</span> +<span class="d">- root.addView(</span> +<span class="d">- HorizontalScrollView(this).apply {</span> +<span class="d">- isHorizontalScrollBarEnabled = false</span> +<span class="d">- addView(icons, LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT))</span> +<span class="d">- },</span> +<span class="d">- LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)</span> +<span class="d">- )</span> +<span class="d">-</span> +<span class="d">- closeOthers = null</span> +<span class="d">- if (recents.isNotEmpty()) {</span> +<span class="d">- // Two tall buttons, one above the other with space between, so that</span> +<span class="d">- // a finger does not hit the wrong one.</span> +<span class="d">- fun bigButton(black: Boolean, onClick: () -> Unit) = TextView(this).apply {</span> +<span class="d">- gravity = Gravity.CENTER</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)</span> +<span class="d">- typeface = Typeface.DEFAULT_BOLD</span> +<span class="d">- setSingleLine()</span> +<span class="d">- setTextColor(if (black) Color.WHITE else Color.BLACK)</span> +<span class="d">- background = GradientDrawable().apply {</span> +<span class="d">- setColor(if (black) Color.BLACK else Color.WHITE)</span> +<span class="d">- setStroke(dp(2), Color.BLACK)</span> +<span class="d">- cornerRadius = dp(8).toFloat()</span> +<span class="d">- }</span> +<span class="d">- setOnClickListener { onClick() }</span> +<span class="d">- }</span> +<span class="d">- fun place(v: View, top: Int) = root.addView(</span> +<span class="d">- v,</span> +<span class="d">- LinearLayout.LayoutParams(MATCH_PARENT, dp(60)).apply { setMargins(dp(20), top, dp(20), 0) }</span> +<span class="d">- )</span> +<span class="d">- if (recents.size > 1) {</span> +<span class="d">- // The name is filled in by show(i): it is the card in the middle.</span> +<span class="d">- closeOthers = bigButton(black = true) { close(recents.filterIndexed { i, _ -> i != index }) }</span> +<span class="d">- place(closeOthers!!, dp(8))</span> +<span class="d">- }</span> +<span class="d">- val all = bigButton(black = false) { close(recents) }.apply { text = "Close all" }</span> +<span class="d">- place(all, dp(20))</span> +<span class="a">+ override fun onCreate(savedInstanceState: Bundle?) {</span> +<span class="a">+ super.onCreate(savedInstanceState)</span> +<span class="a">+ val open = Intent(ACTION_OPEN).setPackage(INK_RECENTS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)</span> +<span class="a">+ if (runCatching { startActivity(open) }.isFailure) {</span> +<span class="a">+ Toast.makeText(this, "Install Ink Recents", Toast.LENGTH_LONG).show()</span> +<span class="a">+ startActivity(Intent(this, HomeHubActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))</span> + } +<span class="d">- setContentView(root)</span> +<span class="d">- scroll.post { show(index); hint() }</span> +<span class="a">+ finish()</span> + } + +<span class="d">- /**</span> +<span class="d">- * Closes apps. With shell access the task leaves the list of the system and</span> +<span class="d">- * its background processes end. Without it, Android ends the background</span> +<span class="d">- * processes and the app leaves the list of this screen.</span> +<span class="d">- */</span> +<span class="d">- private fun close(apps: List<Apps.Recent>) {</span> +<span class="d">- index = 0</span> +<span class="d">- val mine = apps.filter { it.app.pkg != packageName }</span> +<span class="d">- val (withTask, without) = mine.partition { it.taskId != null && Shell.ready }</span> +<span class="d">- without.forEach { Apps.closeWithoutShell(this, it.app.pkg) }</span> +<span class="d">- if (withTask.isEmpty()) return load()</span> +<span class="d">- Shell.runAll(withTask.flatMap { listOf("am stack remove " + it.taskId, "am kill " + it.app.pkg) }) { load() }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private var hinted = false</span> +<span class="d">-</span> +<span class="d">- /**</span> +<span class="d">- * Shows once what a swipe does: the card in the middle jumps up and comes</span> +<span class="d">- * back, then the row jumps sideways and comes back. Four still frames, no</span> +<span class="d">- * glide, so that e-ink draws them clean.</span> +<span class="d">- */</span> +<span class="d">- private fun hint() {</span> +<span class="d">- if (hinted) return</span> +<span class="d">- hinted = true</span> +<span class="d">- val s = scroller ?: return</span> +<span class="d">- val card = row?.getChildAt(index) ?: return</span> +<span class="d">- val many = (row?.childCount ?: 0) > 1</span> +<span class="d">- s.postDelayed({ card.translationY = -dp(28).toFloat() }, 500)</span> +<span class="d">- s.postDelayed({ card.translationY = 0f }, 900)</span> +<span class="d">- if (many) {</span> +<span class="d">- val side = if (index == 0) dp(40) else -dp(40)</span> +<span class="d">- s.postDelayed({ s.scrollTo(index * cardStep + side, 0) }, 1300)</span> +<span class="d">- s.postDelayed({ s.scrollTo(index * cardStep, 0) }, 1700)</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- /** Puts card [i] in the middle, in one step, and says which one it is. */</span> +<span class="d">- private fun show(i: Int) {</span> +<span class="d">- val s = scroller ?: return</span> +<span class="d">- val count = row?.childCount ?: 0</span> +<span class="d">- if (cardStep <= 0 || count == 0) return</span> +<span class="d">- index = i.coerceIn(0, count - 1)</span> +<span class="d">- s.scrollTo(index * cardStep, 0)</span> +<span class="d">- heading?.text = names.getOrElse(index) { "" } + " " + (index + 1) + " of " + count</span> +<span class="d">- closeOthers?.text = "Close all but " + names.getOrElse(index) { "this one" }</span> +<span class="d">- strip?.let { icons -></span> +<span class="d">- for (n in 0 until icons.childCount) {</span> +<span class="d">- icons.getChildAt(n).background = if (n != index) null else GradientDrawable().apply {</span> +<span class="d">- setStroke(dp(2), Color.BLACK)</span> +<span class="d">- cornerRadius = dp(10).toFloat()</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- private fun card(r: Apps.Recent, closable: Boolean): View {</span> +<span class="d">- val box = LinearLayout(this).apply {</span> +<span class="d">- orientation = LinearLayout.VERTICAL</span> +<span class="d">- background = GradientDrawable().apply {</span> +<span class="d">- setColor(Color.WHITE)</span> +<span class="d">- setStroke(dp(2), Color.BLACK)</span> +<span class="d">- cornerRadius = dp(14).toFloat()</span> +<span class="d">- }</span> +<span class="d">- val p = dp(3)</span> +<span class="d">- setPadding(p, p, p, p)</span> +<span class="d">- clipToOutline = true</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- val icon = runCatching { packageManager.getApplicationIcon(r.app.pkg) }.getOrNull()</span> +<span class="d">- box.addView(LinearLayout(this).apply {</span> +<span class="d">- orientation = LinearLayout.HORIZONTAL</span> +<span class="d">- gravity = Gravity.CENTER_VERTICAL</span> +<span class="d">- setPadding(dp(12), dp(10), dp(12), dp(10))</span> +<span class="d">- addView(ImageView(context).apply { setImageDrawable(icon) }, LinearLayout.LayoutParams(dp(32), dp(32)))</span> +<span class="d">- addView(TextView(context).apply {</span> +<span class="d">- text = r.app.label</span> +<span class="d">- setTextColor(Color.BLACK)</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 17f)</span> +<span class="d">- setSingleLine()</span> +<span class="d">- setPadding(dp(10), 0, 0, 0)</span> +<span class="d">- })</span> +<span class="d">- })</span> +<span class="d">- box.addView(View(this).apply { setBackgroundColor(Color.BLACK) }, LinearLayout.LayoutParams(MATCH_PARENT, dp(1)))</span> +<span class="d">-</span> +<span class="d">- // The body: the app icon now, the task picture when and if it arrives.</span> +<span class="d">- val body = ImageView(this).apply {</span> +<span class="d">- setImageDrawable(icon)</span> +<span class="d">- scaleType = ImageView.ScaleType.CENTER_INSIDE</span> +<span class="d">- val p = dp(64)</span> +<span class="d">- setPadding(p, p, p, p)</span> +<span class="d">- }</span> +<span class="d">- box.addView(body, LinearLayout.LayoutParams(MATCH_PARENT, 0, 1f))</span> +<span class="d">- if (r.app.pkg != packageName) {</span> +<span class="d">- box.addView(View(this).apply { setBackgroundColor(Color.BLACK) }, LinearLayout.LayoutParams(MATCH_PARENT, dp(1)))</span> +<span class="d">- box.addView(TextView(this).apply {</span> +<span class="d">- text = "\u2191 close this \u2193 close the others"</span> +<span class="d">- setTextColor(Color.BLACK)</span> +<span class="d">- setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)</span> +<span class="d">- gravity = Gravity.CENTER_HORIZONTAL</span> +<span class="d">- setPadding(0, dp(8), 0, dp(8))</span> +<span class="d">- })</span> +<span class="d">- }</span> +<span class="d">- r.taskId?.let { id -></span> +<span class="d">- Apps.snapshot(id) { picture -></span> +<span class="d">- if (picture != null && !isFinishing) {</span> +<span class="d">- body.setPadding(0, 0, 0, 0)</span> +<span class="d">- body.scaleType = ImageView.ScaleType.FIT_START</span> +<span class="d">- body.setImageBitmap(picture)</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- val gestures = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {</span> +<span class="d">- override fun onDown(e: MotionEvent): Boolean = true</span> +<span class="d">-</span> +<span class="d">- override fun onSingleTapUp(e: MotionEvent): Boolean {</span> +<span class="d">- Apps.launch(this@RecentsActivity, r.app)</span> +<span class="d">- finish()</span> +<span class="d">- return true</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- override fun onFling(e1: MotionEvent?, e2: MotionEvent, vx: Float, vy: Float): Boolean {</span> +<span class="d">- val up = e1 != null && e1.y - e2.y > box.height / 5 && abs(vy) > abs(vx)</span> +<span class="d">- val down = e1 != null && e2.y - e1.y > box.height / 5 && abs(vy) > abs(vx)</span> +<span class="d">- when {</span> +<span class="d">- up -> close(listOf(r))</span> +<span class="d">- down -> close(shown.filter { it !== r })</span> +<span class="d">- else -> return false</span> +<span class="d">- }</span> +<span class="d">- return true</span> +<span class="d">- }</span> +<span class="d">- })</span> +<span class="d">- box.setOnTouchListener { v, e -></span> +<span class="d">- if (e.actionMasked == MotionEvent.ACTION_UP) v.performClick()</span> +<span class="d">- gestures.onTouchEvent(e)</span> +<span class="d">- }</span> +<span class="d">- return box</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- // ---- no access yet ---------------------------------------------------------------------</span> +<span class="d">-</span> +<span class="d">- private fun askForAccess() {</span> +<span class="d">- val col = Ui.page(this)</span> +<span class="d">- col.title("Recent apps")</span> +<span class="d">- col.note("Usage access puts your apps in order of last use.")</span> +<span class="d">- col.button("Give usage access") {</span> +<span class="d">- runCatching { startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) }</span> +<span class="d">- }</span> +<span class="d">- if (Shell.SUPPORTED) {</span> +<span class="d">- col.button("Shell access") { startActivity(Intent(this, ShellActivity::class.java)) }</span> +<span class="d">- }</span> +<span class="a">+ companion object {</span> +<span class="a">+ const val INK_RECENTS = "dev.equwal.inkrecents"</span> +<span class="a">+ const val ACTION_OPEN = "dev.equwal.inkrecents.OPEN"</span> + } + } +diff --git a/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt b/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt +index e1c9216..85bb9c5 100644 +--- a/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/setup/GuidedSetupActivity.kt +<span class="h">@@ -182,9 +182,7 @@ class GuidedSetupActivity : Activity() {</span> + } + when { + HwKey.SCREEN in set -> col.note("A small button floats over every app. Drag to move it.") +<span class="d">- HwKey.POWER in set && set.size > 1 -></span> +<span class="d">- col.note("Hold Power, then press the other button.")</span> +<span class="d">- HwKey.POWER in set && !env.powerIsDirect && !env.shellSupported -></span> +<span class="a">+ set == setOf(HwKey.POWER) && !env.powerIsDirect && !env.shellSupported -></span> + col.note("A tap cannot reach any app. Hold and double tap work.") + } + if (HwKey.SCREEN !in set) { +diff --git a/app/src/main/java/dev/equwal/assistkey/store/SettingsFile.kt b/app/src/main/java/dev/equwal/assistkey/store/SettingsFile.kt +index baf2496..8e297a4 100644 +--- a/app/src/main/java/dev/equwal/assistkey/store/SettingsFile.kt ++++ b/app/src/main/java/dev/equwal/assistkey/store/SettingsFile.kt +<span class="h">@@ -29,7 +29,6 @@ object SettingsFile {</span> + "assistkey" to setOf("bindings", "multitap_ms", "hold_ms", "chord_ms"), + "assistkey_channels" to null, + "assistkey_nav" to setOf("buttons", "gestures", "keys"), +<span class="d">- "assistkey_home" to setOf("favourites", "hidden", "clock", "auto_open"),</span> + "assistkey_voice" to setOf("engine", "language"), + "assistkey_device" to setOf("ai_key_returns"), + "assistkey_power" to setOf("wanted") +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt +index a38ffda..552dff7 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/ActionPickerActivity.kt +<span class="h">@@ -114,21 +114,9 @@ class ActionPickerActivity : Activity() {</span> + + private fun own(col: LinearLayout) { + col.header("Rebind") +<span class="d">- listOf(</span> +<span class="d">- "Recent apps (cards)" to "dev.equwal.assistkey.home.RecentsActivity",</span> +<span class="d">- "App search" to "dev.equwal.assistkey.home.HomeActivity"</span> +<span class="d">- ).filter { (_, cls) -></span> +<span class="d">- // The home screen is switched off until asked for; a disabled</span> +<span class="d">- // activity cannot be launched, so it is not offered.</span> +<span class="d">- packageManager.getComponentEnabledSetting(android.content.ComponentName(packageName, cls)) !=</span> +<span class="d">- android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED &&</span> +<span class="d">- (cls.endsWith("RecentsActivity") ||</span> +<span class="d">- packageManager.getComponentEnabledSetting(android.content.ComponentName(packageName, cls)) ==</span> +<span class="d">- android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED)</span> +<span class="d">- }.forEach { (label, cls) -></span> +<span class="d">- col.row(label, null) {</span> +<span class="d">- choose(ActionSpec(ActionKind.LAUNCH_COMPONENT, packageName + "/" + cls, label))</span> +<span class="d">- }</span> +<span class="a">+ val label = "Recent apps (cards)"</span> +<span class="a">+ col.row(label, null) {</span> +<span class="a">+ choose(ActionSpec(ActionKind.LAUNCH_COMPONENT, packageName + "/dev.equwal.assistkey.home.RecentsActivity", label))</span> + } + } + +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/BundledRows.kt b/app/src/main/java/dev/equwal/assistkey/ui/BundledRows.kt +new file mode 100644 +index 0000000..7bc8329 +--- /dev/null ++++ b/app/src/main/java/dev/equwal/assistkey/ui/BundledRows.kt +<span class="h">@@ -0,0 +1,28 @@</span> +<span class="a">+package dev.equwal.assistkey.ui</span> +<span class="a">+</span> +<span class="a">+import android.app.Activity</span> +<span class="a">+import android.widget.LinearLayout</span> +<span class="a">+import dev.equwal.assistkey.bundle.Bundled</span> +<span class="a">+import dev.equwal.assistkey.ui.Ui.header</span> +<span class="a">+import dev.equwal.assistkey.ui.Ui.row</span> +<span class="a">+</span> +<span class="a">+/** The apps of other makers that belong on one screen: installed, or one tap to get. */</span> +<span class="a">+object BundledRows {</span> +<span class="a">+</span> +<span class="a">+ fun add(a: Activity, col: LinearLayout, group: String) {</span> +<span class="a">+ val items = Bundled.items.filter { it.group == group }</span> +<span class="a">+ if (items.isEmpty()) return</span> +<span class="a">+ col.header("Apps of other makers")</span> +<span class="a">+ items.forEach { item -></span> +<span class="a">+ val installed = runCatching { a.packageManager.getPackageInfo(item.pkg, 0) }.isSuccess</span> +<span class="a">+ col.row(</span> +<span class="a">+ item.title,</span> +<span class="a">+ item.hint + " · " + item.version + " · " + item.licence,</span> +<span class="a">+ state = if (installed) "Installed" else if (Bundled.SUPPORTED) "Install" else "Get it"</span> +<span class="a">+ ) {</span> +<span class="a">+ if (!installed) Bundled.install(a, item)</span> +<span class="a">+ else a.packageManager.getLaunchIntentForPackage(item.pkg)?.let(a::startActivity)</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt +index 56c7e18..f82e7a0 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt +<span class="h">@@ -35,12 +35,12 @@ class HacksActivity : Activity() {</span> + + private fun build() { + val col = Ui.page(this, "Hardware hacks") +<span class="d">- fun door(title: String, hint: String, ch: Channel) = col.row(</span> +<span class="d">- title, hint, state = if (Channels.isSatisfied(this, ch)) "On" else "Off"</span> +<span class="a">+ fun door(title: String, hint: String, vararg ways: Channel) = col.row(</span> +<span class="a">+ title, hint, state = if (ways.any { Channels.isSatisfied(this, it) }) "On" else "Off"</span> + ) { startActivity(Intent(this, PowerActivity::class.java)) } + door("Hold Power", "Through the assistant role", Channel.ASSISTANT) +<span class="d">- door("Double tap Power", "Through the camera intent. No camera button.", Channel.CAMERA)</span> +<span class="d">- door("Wallet button", "Through the wallet role", Channel.WALLET)</span> +<span class="a">+ // One gesture, two ways in. A device sends the double tap to the camera or to the wallet.</span> +<span class="a">+ door("Double tap Power", "Through the camera or the wallet intent", Channel.CAMERA, Channel.WALLET)</span> + if (Shell.SUPPORTED) { + col.row( + "Full Power button", +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/HomeHubActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/HomeHubActivity.kt +index f5ba60e..07c8a74 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/HomeHubActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/HomeHubActivity.kt +<span class="h">@@ -1,15 +1,11 @@</span> + package dev.equwal.assistkey.ui + + import android.app.Activity +<span class="d">-import android.content.ComponentName</span> + import android.content.Intent +<span class="d">-import android.content.pm.PackageManager</span> +<span class="d">-import dev.equwal.assistkey.home.HomeActivity</span> +<span class="d">-import dev.equwal.assistkey.home.HomeSettingsActivity</span> +<span class="d">-import dev.equwal.assistkey.home.RecentsActivity</span> +<span class="a">+import dev.equwal.assistkey.bundle.Bundled</span> + import dev.equwal.assistkey.ui.Ui.row + +<span class="d">-/** The two screens AssistKey draws for itself: the home screen, and recents. */</span> +<span class="a">+/** The home screen and the recent-apps screen. Both are apps of their own, which this screen offers. */</span> + class HomeHubActivity : Activity() { + + override fun onResume() { +<span class="h">@@ -20,24 +16,6 @@ class HomeHubActivity : Activity() {</span> + private fun build() { + val col = Ui.page(this, "Home and recents") + +<span class="d">- val offered = packageManager.getComponentEnabledSetting(</span> +<span class="d">- ComponentName(this, HomeActivity::class.java)</span> +<span class="d">- ) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED</span> +<span class="d">- val isDefault = packageManager.resolveActivity(</span> +<span class="d">- Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),</span> +<span class="d">- PackageManager.MATCH_DEFAULT_ONLY</span> +<span class="d">- )?.activityInfo?.packageName == packageName</span> +<span class="d">-</span> +<span class="d">- col.row(</span> +<span class="d">- "Home screen",</span> +<span class="d">- "A clock, your apps, and a search line",</span> +<span class="d">- state = if (isDefault) "In use" else if (offered) "Ready" else "Off"</span> +<span class="d">- ) { startActivity(Intent(this, HomeSettingsActivity::class.java)) }</span> +<span class="d">-</span> +<span class="d">- col.row(</span> +<span class="d">- "Recent apps",</span> +<span class="d">- "Cards you can bind to a button"</span> +<span class="d">- ) { startActivity(Intent(this, RecentsActivity::class.java)) }</span> +<span class="d">-</span> +<span class="a">+ BundledRows.add(this, col, Bundled.HOME)</span> + } + } +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt +index 80e6162..1b69096 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt +<span class="h">@@ -14,7 +14,6 @@ import android.widget.TextView</span> + import dev.equwal.assistkey.channel.Channel + import dev.equwal.assistkey.channel.Channels + import dev.equwal.assistkey.device.Detect +<span class="d">-import dev.equwal.assistkey.home.HomeActivity</span> + import dev.equwal.assistkey.license.License + import dev.equwal.assistkey.license.PlayBilling + import dev.equwal.assistkey.model.GestureType +<span class="h">@@ -174,16 +173,11 @@ class MainActivity : Activity() {</span> + ) + } + +<span class="d">- private fun homeSummary(): String {</span> +<span class="d">- val cn = android.content.ComponentName(this, HomeActivity::class.java)</span> +<span class="d">- val offered = packageManager.getComponentEnabledSetting(cn) ==</span> +<span class="d">- android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED</span> +<span class="d">- val isDefault = packageManager.resolveActivity(</span> +<span class="d">- Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),</span> +<span class="d">- android.content.pm.PackageManager.MATCH_DEFAULT_ONLY</span> +<span class="d">- )?.activityInfo?.packageName == packageName</span> +<span class="d">- return Summary.homeAndRecents(offered, isDefault)</span> +<span class="d">- }</span> +<span class="a">+ private fun homeSummary(): String = Summary.homeAndRecents(</span> +<span class="a">+ listOf("app.clauncher", "dev.equwal.inkrecents").count { pkg -></span> +<span class="a">+ runCatching { packageManager.getPackageInfo(pkg, 0) }.isSuccess</span> +<span class="a">+ }</span> +<span class="a">+ )</span> + + private fun setupSummary(): String { + val granted = PermissionsActivity.granted(this) +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/PermissionsActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/PermissionsActivity.kt +index 3b4648b..5a2e8d8 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/PermissionsActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/PermissionsActivity.kt +<span class="h">@@ -8,7 +8,6 @@ import android.provider.Settings</span> + import android.widget.LinearLayout + import dev.equwal.assistkey.channel.Channel + import dev.equwal.assistkey.channel.Channels +<span class="d">-import dev.equwal.assistkey.home.Apps</span> + import dev.equwal.assistkey.native.PowerNative + import dev.equwal.assistkey.shell.Shell + import dev.equwal.assistkey.ui.Ui.button +<span class="h">@@ -71,11 +70,6 @@ class PermissionsActivity : Activity() {</span> + "For Voice typing", + state[2] + ) { requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 1) } +<span class="d">- list += Item(</span> +<span class="d">- "App usage data",</span> +<span class="d">- "Puts the recent apps cards in order",</span> +<span class="d">- state[3]</span> +<span class="d">- ) { start(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) }</span> + return list + } + +<span class="h">@@ -160,8 +154,7 @@ class PermissionsActivity : Activity() {</span> + fun granted(c: Context): List<Boolean> = listOf( + Channels.isSatisfied(c, Channel.ACCESSIBILITY), + Channels.isSatisfied(c, Channel.ASSISTANT), +<span class="d">- Dictation.hasMicrophone(c),</span> +<span class="d">- Apps.hasUsageAccess(c) || Shell.ready</span> +<span class="a">+ Dictation.hasMicrophone(c)</span> + ) + + /** Opens this screen the first time the app runs, and never again by itself. */ +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt b/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt +index a4c6c95..216e3cd 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/Summary.kt +<span class="h">@@ -50,11 +50,11 @@ object Summary {</span> + else -> "Ready" + } + +<span class="d">- /** The home screen and the recent-apps cards. */</span> +<span class="d">- fun homeAndRecents(homeOffered: Boolean, isDefault: Boolean): String = when {</span> +<span class="d">- isDefault -> "Home screen in use"</span> +<span class="d">- homeOffered -> "Home screen ready"</span> +<span class="d">- else -> "Recent apps only"</span> +<span class="a">+ /** CLauncher and Ink Recents: how many of the two are installed. */</span> +<span class="a">+ fun homeAndRecents(installed: Int): String = when (installed) {</span> +<span class="a">+ 0 -> "CLauncher, Ink Recents"</span> +<span class="a">+ 2 -> "Both installed"</span> +<span class="a">+ else -> "1 of 2 installed"</span> + } + + /** +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/VoiceActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/VoiceActivity.kt +index d522326..04015ea 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/VoiceActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/VoiceActivity.kt +<span class="h">@@ -8,6 +8,7 @@ import android.provider.Settings</span> + import dev.equwal.assistkey.channel.Channel + import dev.equwal.assistkey.channel.Channels + import dev.equwal.assistkey.voice.Dictation +<span class="a">+import dev.equwal.assistkey.bundle.Bundled</span> + import dev.equwal.assistkey.ui.Ui.button + import dev.equwal.assistkey.ui.Ui.header + import dev.equwal.assistkey.ui.Ui.note +<span class="h">@@ -58,18 +59,9 @@ class VoiceActivity : Activity() {</span> + } + } + if (engines.none(Dictation::isOnDevice)) { +<span class="d">- col.note("No app here recognises speech offline. Whisper, from F-Droid, is free.")</span> +<span class="d">- col.button("Get Whisper") {</span> +<span class="d">- val pages = listOf(</span> +<span class="d">- "market://details?id=" + Dictation.WHISPER_PACKAGE,</span> +<span class="d">- "https://f-droid.org/packages/" + Dictation.WHISPER_PACKAGE + "/"</span> +<span class="d">- )</span> +<span class="d">- pages.firstOrNull { url -></span> +<span class="d">- runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) }.isSuccess</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- col.note("In Whisper, download a model and allow the microphone.")</span> +<span class="a">+ col.note("No app here recognises speech offline. Whisper does, and it is free.")</span> + } +<span class="a">+ BundledRows.add(this, col, Bundled.VOICE)</span> + + col.header("Language") + val lang = Dictation.language(this) +diff --git a/app/src/play/java/dev/equwal/assistkey/bundle/Bundled.kt b/app/src/play/java/dev/equwal/assistkey/bundle/Bundled.kt +new file mode 100644 +index 0000000..c1d2477 +--- /dev/null ++++ b/app/src/play/java/dev/equwal/assistkey/bundle/Bundled.kt +<span class="h">@@ -0,0 +1,49 @@</span> +<span class="a">+package dev.equwal.assistkey.bundle</span> +<span class="a">+</span> +<span class="a">+import android.app.Activity</span> +<span class="a">+import android.content.Intent</span> +<span class="a">+import android.net.Uri</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * The Google Play build carries no apps of other makers and cannot install</span> +<span class="a">+ * any: Google Play allows that only for an app whose main purpose it is. It</span> +<span class="a">+ * names the same apps and opens the page of the maker.</span> +<span class="a">+ */</span> +<span class="a">+object Bundled {</span> +<span class="a">+</span> +<span class="a">+ const val SUPPORTED = false</span> +<span class="a">+</span> +<span class="a">+ class Item(</span> +<span class="a">+ /** The screen that lists the app: HOME or VOICE. */</span> +<span class="a">+ val group: String,</span> +<span class="a">+ val title: String,</span> +<span class="a">+ val hint: String,</span> +<span class="a">+ val pkg: String,</span> +<span class="a">+ val version: String,</span> +<span class="a">+ val licence: String,</span> +<span class="a">+ val source: String</span> +<span class="a">+ )</span> +<span class="a">+</span> +<span class="a">+ const val HOME = "home"</span> +<span class="a">+ const val VOICE = "voice"</span> +<span class="a">+</span> +<span class="a">+ val items = listOf(</span> +<span class="a">+ Item(</span> +<span class="a">+ HOME, "CLauncher", "Minimal home screen", "app.clauncher", "v5.3.0", "GPL-3.0",</span> +<span class="a">+ "https://github.com/mlm-games/CLauncher"</span> +<span class="a">+ ),</span> +<span class="a">+ Item(</span> +<span class="a">+ HOME, "Ink Recents", "Recent apps, drawn for e-ink", "dev.equwal.inkrecents", "0.1.1", "GPL-3.0",</span> +<span class="a">+ "https://github.com/equwal/ink-recents"</span> +<span class="a">+ ),</span> +<span class="a">+ Item(</span> +<span class="a">+ VOICE, "Whisper", "Speech to text on the device", "org.woheller69.whisper", "3.7", "MIT",</span> +<span class="a">+ "https://f-droid.org/packages/org.woheller69.whisper/"</span> +<span class="a">+ )</span> +<span class="a">+ )</span> +<span class="a">+</span> +<span class="a">+ /** Opens the page where the maker offers the app. */</span> +<span class="a">+ fun install(a: Activity, item: Item) {</span> +<span class="a">+ runCatching { a.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(item.source))) }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +diff --git a/app/src/test/java/dev/equwal/assistkey/home/RecentsOpenTest.kt b/app/src/test/java/dev/equwal/assistkey/home/RecentsOpenTest.kt +deleted file mode 100644 +index 3ede8e0..0000000 +--- a/app/src/test/java/dev/equwal/assistkey/home/RecentsOpenTest.kt ++++ /dev/null +<span class="h">@@ -1,30 +0,0 @@</span> +<span class="d">-package dev.equwal.assistkey.home</span> +<span class="d">-</span> +<span class="d">-import org.junit.Assert.assertEquals</span> +<span class="d">-import org.junit.Assert.assertTrue</span> +<span class="d">-import org.junit.Test</span> +<span class="d">-import java.util.Random</span> +<span class="d">-</span> +<span class="d">-class RecentsOpenTest {</span> +<span class="d">-</span> +<span class="d">- @Test fun `a closed app stays off the list until it is opened again`() {</span> +<span class="d">- val used = mapOf("reader" to 100L, "notes" to 200L, "mail" to 300L)</span> +<span class="d">- assertEquals(listOf("mail", "notes", "reader"), Apps.open(used, emptyMap()))</span> +<span class="d">- assertEquals(listOf("mail", "reader"), Apps.open(used, mapOf("notes" to 250L)))</span> +<span class="d">- // Opened again after the close: back on the list.</span> +<span class="d">- assertEquals(listOf("mail", "notes", "reader"), Apps.open(used, mapOf("notes" to 150L)))</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- @Test fun `the list is newest first, has no app twice, and has no app closed after its last use`() {</span> +<span class="d">- val r = Random(11)</span> +<span class="d">- repeat(500) {</span> +<span class="d">- val used = (0 until r.nextInt(8)).associate { "app" + r.nextInt(10) to r.nextInt(1000).toLong() }</span> +<span class="d">- val closed = (0 until r.nextInt(6)).associate { "app" + r.nextInt(10) to r.nextInt(1000).toLong() }</span> +<span class="d">- val shown = Apps.open(used, closed)</span> +<span class="d">- assertEquals(shown.distinct(), shown)</span> +<span class="d">- assertEquals(shown.sortedByDescending { used.getValue(it) }.map { used.getValue(it) }, shown.map { used.getValue(it) })</span> +<span class="d">- assertTrue(shown.all { used.getValue(it) > (closed[it] ?: Long.MIN_VALUE) })</span> +<span class="d">- assertTrue(used.keys.filter { it !in shown }.all { used.getValue(it) <= closed.getValue(it) })</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">-}</span> +diff --git a/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt b/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt +index ad18b13..3dcebdd 100644 +--- a/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt ++++ b/app/src/test/java/dev/equwal/assistkey/ui/SummaryTest.kt +<span class="h">@@ -55,9 +55,9 @@ class SummaryTest {</span> + + @Test + fun `home and recents says what is in use`() { +<span class="d">- assertEquals("Home screen in use", Summary.homeAndRecents(homeOffered = true, isDefault = true))</span> +<span class="d">- assertEquals("Home screen ready", Summary.homeAndRecents(homeOffered = true, isDefault = false))</span> +<span class="d">- assertEquals("Recent apps only", Summary.homeAndRecents(homeOffered = false, isDefault = false))</span> +<span class="a">+ assertEquals("CLauncher, Ink Recents", Summary.homeAndRecents(0))</span> +<span class="a">+ assertEquals("1 of 2 installed", Summary.homeAndRecents(1))</span> +<span class="a">+ assertEquals("Both installed", Summary.homeAndRecents(2))</span> + } + + @Test +diff --git a/gradle.properties b/gradle.properties +index dd01f89..8f7de54 100644 +--- a/gradle.properties ++++ b/gradle.properties +<span class="h">@@ -6,8 +6,8 @@ android.nonTransitiveRClass=true</span> + kotlin.code.style=official + + # Release identity. versionCode must rise with every upload to Play. +<span class="d">-assistkey.versionName=0.0.11-alpha</span> +<span class="d">-assistkey.versionCode=11</span> +<span class="a">+assistkey.versionName=0.0.12-alpha</span> +<span class="a">+assistkey.versionCode=12</span> + + # The free beta ends on this date (UTC) at the latest. It is the backstop for + # installs that cannot reach Google Play; the real switch is the +diff --git a/play/DECLARATIONS.md b/play/DECLARATIONS.md +index fa75a87..0eaaf93 100644 +--- a/play/DECLARATIONS.md ++++ b/play/DECLARATIONS.md +<span class="h">@@ -44,7 +44,7 @@ so it is not a financial feature.)</span> + + Why "No" is accurate: the app holds no `INTERNET` permission + (`aapt2 dump badging` on the release shows `WRITE_SECURE_SETTINGS`, +<span class="d">-`PACKAGE_USAGE_STATS`, `RECORD_AUDIO`, `QUICK_ACCESS_WALLET` and</span> +<span class="a">+`RECORD_AUDIO`, `QUICK_ACCESS_WALLET` and</span> + `com.android.vending.BILLING`, none of which is network access). These answers + are for the `play` build, which is the only one uploaded. The device report is + composed on the device and leaves it only if the user sends it, through their +<span class="h">@@ -93,7 +93,6 @@ pointed at the reader - the reader cannot capture its own screen.</span> + | `BIND_ACCESSIBILITY_SERVICE` | Above. | + | `WRITE_SECURE_SETTINGS` | Cannot be granted to a Play install; it does nothing unless the owner grants it over adb. It then lets the app switch the firmware's power-button behaviour (short press, hold duration). Declared so that the grant is possible at all. | + | `RECORD_AUDIO` | Voice typing only. Requested at run time from the Voice typing screen. The speech recognition app the user chose records the audio; Android requires the calling app to hold the permission too. AssistKey receives text only, and stores and sends nothing. | +<span class="d">-| `PACKAGE_USAGE_STATS` | Special access the user grants from a settings screen. Used only by the recent-apps list to order apps by last use, on the device; nothing is stored or sent. Not needed when shell access is on. |</span> + | `QUICK_ACCESS_WALLET` | Required of any app offered as the wallet app. AssistKey serves an empty card list; it exists so a press of the wallet shortcut reaches the user's chosen action. | + | `com.android.vending.BILLING` | Play Billing. | + +diff --git a/play/LISTING.md b/play/LISTING.md +index f97b1f0..40f9bd7 100644 +--- a/play/LISTING.md ++++ b/play/LISTING.md +<span class="h">@@ -65,8 +65,7 @@ VOICE TYPING</span> + Bind a key to Voice typing: press, speak, and the words appear where the cursor is - in any app, with the keyboard you already have. Rebind has no speech engine and no internet access; a speech recognition app on your device does the listening. + + MORE FOR E-INK +<span class="d">-• A plain text home screen: a clock, a few apps, type to find the rest</span> +<span class="d">-• Recent apps as swipe cards, drawn for e-ink with no animation</span> +<span class="a">+• Opens Ink Recents, our free recent-apps app for e-ink, from any button</span> + + BUILT FOR E-INK + +<span class="h">@@ -88,7 +87,7 @@ GOOD TO KNOW</span> + • If a volume key does not respond, the device's own key settings may be holding on to it. The built-in key tester shows exactly which keys Rebind can see, and the Firmware key hooks screen shows the fix. + + COMPATIBILITY +<span class="d">-Works on Android 12 and later. Built and tested on the Viwoods AiPaper Reader; key remapping, page turning, voice typing and the home screen work on Boox, Bigme, Meebook and ordinary phones and tablets too. Search terms people use for this: button mapper, key remapper, remap buttons, volume key page turn, double tap, long press, key combination.</span> +<span class="a">+Works on Android 12 and later. Built and tested on the Viwoods AiPaper Reader; key remapping, page turning, and voice typing work on Boox, Bigme, Meebook and ordinary phones and tablets too. Search terms people use for this: button mapper, key remapper, remap buttons, volume key page turn, double tap, long press, key combination.</span> + + Rebind is not affiliated with, or endorsed by, Viwoods, Onyx Boox, Bigme or Meebook. + +diff --git a/play/PRIVACY.md b/play/PRIVACY.md +index f61a26c..ede5cec 100644 +--- a/play/PRIVACY.md ++++ b/play/PRIVACY.md +<span class="h">@@ -34,12 +34,9 @@ field that has the cursor. To do that it reads the current text of that one</span> + field at that moment. It keeps and sends none of it, and never writes into + password fields. + +<span class="d">-**Recent apps.** If you use the recent-apps list without shell access, it</span> +<span class="d">-reads the usage log Android keeps, with the usage access you grant, to put apps</span> +<span class="d">-in order of last use. This happens on the device each time the list opens;</span> +<span class="d">-nothing from it is stored or sent.</span> +<span class="a">+**Recent apps.** Rebind has no recent-apps list of its own. The Recent apps action opens Ink Recents, a separate app with a privacy policy of its own.</span> + +<span class="d">-**Closing apps.** When you close an app in the recent-apps list, Rebind asks Android to end the background processes of that app, and remembers on your device the time you closed it, so that the app stays off the list until you open it again.</span> +<span class="a">+**Apps of other makers.** The build for direct install carries the release files of three other apps (CLauncher, Ink Recents, Whisper) and can hand them to the installer of Android, which asks you first. Rebind downloads nothing. Each of those apps has its own policy. The Google Play build carries none of them.</span> + + **The on-screen button.** If you give the on-screen button an action, the accessibility service draws a small round button over other apps. It reads nothing from the screen. It remembers only where you put it. + </pre> +</div></body></html> diff --git a/git/assistkey/commit/09ac2ef42c0cfe4ac02cb643b0fc4f27fc112264.html b/git/assistkey/commit/09ac2ef42c0cfe4ac02cb643b0fc4f27fc112264.html new file mode 100644 index 0000000..393dd51 --- /dev/null +++ b/git/assistkey/commit/09ac2ef42c0cfe4ac02cb643b0fc4f27fc112264.html @@ -0,0 +1,186 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>assistkey 09ac2ef4 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>assistkey</h1><span class="desc">Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</span><p class="url">git clone https://github.com/equwal/assistkey</p><p><a href="../../assistkey/index.html">Log</a> | <a href="../../assistkey/files.html">Files</a> | <a href="../../assistkey/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 09ac2ef42c0cfe4ac02cb643b0fc4f27fc112264 +truex <truex@equwal.com> +2026-09-20 22:27:51 -0700 + +Fix: a Power press on a sleeping or locked device always wakes and unlocks it + +While the app manages the Power button, the firmware reaction to a short press +is off. A press on a sleeping or locked device then had only the firmware's own +wake to rely on, and the press could also run the tap action (Back). + +KeyFilterService now treats such a press as a wake and never as a gesture. It +tracks SCREEN_OFF and SCREEN_ON, because the firmware can set isInteractive +before the press reaches this process. It also sends KEYCODE_WAKEUP and +`wm dismiss-keyguard` through the shell, so the wake does not depend on the +firmware. A press within one second after the screen comes on is the press that +woke it, and is ignored. + +Regression test on a device: tools/e2e/power-wake.sh. + +</pre><pre> .../equwal/assistkey/engine/KeyFilterService.kt | 57 ++++++++++++++++++---- + tools/e2e/power-wake.sh | 49 +++++++++++++++++++ + 2 files changed, 96 insertions(+), 10 deletions(-) +</pre><pre>diff --git a/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt b/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt +index 9d0b5fd..bd87f80 100644 +--- a/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt ++++ b/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt +<span class="h">@@ -46,7 +46,12 @@ class KeyFilterService : AccessibilityService(), GestureEngine.Host {</span> + + Shell.onChange(shellChanged) + Store.onBindingsChanged = { syncPower() } +<span class="d">- registerReceiver(screenOn, android.content.IntentFilter(android.content.Intent.ACTION_SCREEN_ON))</span> +<span class="a">+ registerReceiver(</span> +<span class="a">+ screenState,</span> +<span class="a">+ android.content.IntentFilter(android.content.Intent.ACTION_SCREEN_ON).apply {</span> +<span class="a">+ addAction(android.content.Intent.ACTION_SCREEN_OFF)</span> +<span class="a">+ }</span> +<span class="a">+ )</span> + Shell.connect(this) + syncPower() + } +<span class="h">@@ -56,32 +61,61 @@ class KeyFilterService : AccessibilityService(), GestureEngine.Host {</span> + private val shellChanged: () -> Unit = { syncPower() } + private var powerIgnoredUntilUp = false + +<span class="d">- /** A press that wakes the reader is not a gesture. */</span> +<span class="d">- private val screenOn = object : android.content.BroadcastReceiver() {</span> +<span class="a">+ /**</span> +<span class="a">+ * True from the moment the screen goes off until it is on again. The</span> +<span class="a">+ * broadcast for "off" arrives before the device sleeps, so this is a better</span> +<span class="a">+ * witness than PowerManager.isInteractive, which the firmware may already</span> +<span class="a">+ * have flipped by the time a Power press reaches this process.</span> +<span class="a">+ */</span> +<span class="a">+ private var screenOff = false</span> +<span class="a">+ private var wokeAt = 0L</span> +<span class="a">+</span> +<span class="a">+ private val screenState = object : android.content.BroadcastReceiver() {</span> + override fun onReceive(c: android.content.Context?, i: android.content.Intent?) { +<span class="a">+ if (i?.action == android.content.Intent.ACTION_SCREEN_OFF) {</span> +<span class="a">+ screenOff = true</span> +<span class="a">+ } else {</span> +<span class="a">+ screenOff = false</span> +<span class="a">+ wokeAt = SystemClock.uptimeMillis()</span> +<span class="a">+ dev.equwal.assistkey.display.ExtraDim.reapply(this@KeyFilterService)</span> +<span class="a">+ }</span> + powerIgnoredUntilUp = true + if (::engine.isInitialized) engine.onCancel() +<span class="d">- dev.equwal.assistkey.display.ExtraDim.reapply(this@KeyFilterService)</span> + } + } + + fun syncPower() = PowerControl.sync(this, serviceRunning = ServiceHolder.service === this, ::onRawPower) + +<span class="a">+ /**</span> +<span class="a">+ * A Power press on a sleeping or locked device has one job: wake it and</span> +<span class="a">+ * unlock it. It is never a gesture. The firmware normally wakes the device</span> +<span class="a">+ * by itself, but the button is ours now, so this does not rely on that: the</span> +<span class="a">+ * wake and the keyguard dismissal are sent through the shell as well.</span> +<span class="a">+ */</span> + private fun onRawPower(down: Boolean, at: Long) { + if (!::engine.isInitialized) return +<span class="d">- if (down) {</span> +<span class="d">- val awake = getSystemService(android.os.PowerManager::class.java)?.isInteractive != false</span> +<span class="d">- powerIgnoredUntilUp = !awake</span> +<span class="d">- if (awake) engine.onDown(HwKey.POWER, at, 0)</span> +<span class="d">- } else {</span> +<span class="a">+ if (!down) {</span> + if (powerIgnoredUntilUp) powerIgnoredUntilUp = false else engine.onUp(HwKey.POWER, at) +<span class="a">+ return</span> +<span class="a">+ }</span> +<span class="a">+ val asleep = screenOff ||</span> +<span class="a">+ getSystemService(android.os.PowerManager::class.java)?.isInteractive == false</span> +<span class="a">+ val locked = getSystemService(android.app.KeyguardManager::class.java)?.isKeyguardLocked == true</span> +<span class="a">+ val justWoke = SystemClock.uptimeMillis() - wokeAt < WAKE_GRACE_MS</span> +<span class="a">+ if (asleep || locked || justWoke) {</span> +<span class="a">+ powerIgnoredUntilUp = true</span> +<span class="a">+ engine.onCancel()</span> +<span class="a">+ if (asleep || locked) Shell.run("input keyevent KEYCODE_WAKEUP; wm dismiss-keyguard")</span> +<span class="a">+ return</span> + } +<span class="a">+ powerIgnoredUntilUp = false</span> +<span class="a">+ engine.onDown(HwKey.POWER, at, 0)</span> + } + + private fun stopPower() { + Shell.removeOnChange(shellChanged) + Store.onBindingsChanged = null +<span class="d">- runCatching { unregisterReceiver(screenOn) }</span> +<span class="a">+ runCatching { unregisterReceiver(screenState) }</span> + ServiceHolder.service = null + syncPower() // with no service running this hands Power back to the firmware + } +<span class="h">@@ -220,6 +254,9 @@ class KeyFilterService : AccessibilityService(), GestureEngine.Host {</span> + /** How long after a Power hold a key press still counts as a combination. */ + const val POWER_COMBO_WINDOW_MS = 1000L + +<span class="a">+ /** A press this soon after the screen came on is the press that woke it. */</span> +<span class="a">+ private const val WAKE_GRACE_MS = 1000L</span> +<span class="a">+</span> + private val LIFELINE = setOf( + GlobalAction.BACK.name, GlobalAction.HOME.name, GlobalAction.RECENTS.name + ) +diff --git a/tools/e2e/power-wake.sh b/tools/e2e/power-wake.sh +new file mode 100644 +index 0000000..8508ea1 +--- /dev/null ++++ b/tools/e2e/power-wake.sh +<span class="h">@@ -0,0 +1,49 @@</span> +<span class="a">+#!/usr/bin/env bash</span> +<span class="a">+# Regression test, on a device: in managed Power mode a Power press on a sleeping</span> +<span class="a">+# device wakes it, and is NOT read as a gesture.</span> +<span class="a">+#</span> +<span class="a">+# Bug it guards: with tap bound to Back, the press that woke the reader could</span> +<span class="a">+# also run Back, and a locked or charging reader had no sure way to wake.</span> +<span class="a">+#</span> +<span class="a">+# Needs: adb root, release build installed, key remapping on, shell access on,</span> +<span class="a">+# Power tap bound to Back and triple tap to Lock screen (Navigation > Power key). Presses go through the kernel input</span> +<span class="a">+# node, because `adb shell input keyevent` skips the input filter stage.</span> +<span class="a">+set -u</span> +<span class="a">+export MSYS_NO_PATHCONV=1</span> +<span class="a">+NODE=${POWER_NODE:-/dev/input/event1}</span> +<span class="a">+E="sendevent $NODE"; TAP="$E 1 116 1; $E 0 0 0; sleep 0.06; $E 1 116 0; $E 0 0 0"</span> +<span class="a">+wake() { adb shell dumpsys power | grep -oE 'mWakefulness=[A-Za-z]+' | head -1 | cut -d= -f2; }</span> +<span class="a">+top() { # dumpsys now and then answers before the activity has settled; ask again.</span> +<span class="a">+ local t="" i</span> +<span class="a">+ for i in 1 2 3 4 5; do</span> +<span class="a">+ t=$(adb shell "dumpsys activity activities" | grep -m1 topResumedActivity | grep -oE '[A-Za-z0-9_.]+/[A-Za-z0-9_.$]+' | head -1)</span> +<span class="a">+ [ -n "$t" ] && break; sleep 1</span> +<span class="a">+ done; echo "$t"; }</span> +<span class="a">+fail=0</span> +<span class="a">+check() { if [ "$2" = "$3" ]; then echo "PASS $1: $2"; else echo "FAIL $1: got '$2', want '$3'"; fail=1; fi; }</span> +<span class="a">+</span> +<span class="a">+[ "$(adb shell settings get global power_button_short_press | tr -d '\r')" = "0" ] || { echo "SKIP Power is not managed (power_button_short_press is not 0)"; exit 2; }</span> +<span class="a">+</span> +<span class="a">+adb shell input keyevent KEYCODE_WAKEUP; sleep 2 # let any earlier tap window run out</span> +<span class="a">+# Two AssistKey screens deep, so that Back would visibly change the top activity.</span> +<span class="a">+# (Not the Settings app: on the Viwoods reader an injected SLEEP key is ignored</span> +<span class="a">+# while Settings > Display is in front.)</span> +<span class="a">+P=dev.equwal.assistkey</span> +<span class="a">+adb shell "am start -n $P/.ui.MainActivity" >/dev/null 2>&1; sleep 2</span> +<span class="a">+adb shell "su 0 am start -n $P/.ui.TimingActivity" >/dev/null 2>&1; sleep 2.5</span> +<span class="a">+before=$(top)</span> +<span class="a">+</span> +<span class="a">+# Sleep with the triple tap the app binds to Lock screen. An injected SLEEP key</span> +<span class="a">+# is not reliable on this firmware.</span> +<span class="a">+QUICK="$E 1 116 1; $E 0 0 0; $E 1 116 0; $E 0 0 0;"</span> +<span class="a">+adb shell "su 0 sh -c '$QUICK sleep 0.05; $QUICK sleep 0.05; $QUICK'"; sleep 4</span> +<span class="a">+check "device sleeps" "$(wake | sed 's/Dozing/Asleep/')" "Asleep"</span> +<span class="a">+</span> +<span class="a">+adb shell "su 0 sh -c '$TAP'"; sleep 3</span> +<span class="a">+check "one Power press wakes it" "$(wake)" "Awake"</span> +<span class="a">+check "the waking press is not a Back gesture" "$(top)" "$before"</span> +<span class="a">+</span> +<span class="a">+adb shell "su 0 sh -c '$TAP'"; sleep 2.5</span> +<span class="a">+[ "$(top)" != "$before" ] && echo "PASS the next press is a gesture again" || { echo "FAIL a press while awake did nothing"; fail=1; }</span> +<span class="a">+adb shell input keyevent KEYCODE_HOME</span> +<span class="a">+exit $fail</span></pre> +</div></body></html> diff --git a/git/assistkey/commit/1030d2b2c355341a737cc75a6d72dc054e70456f.html b/git/assistkey/commit/1030d2b2c355341a737cc75a6d72dc054e70456f.html new file mode 100644 index 0000000..6b5d668 --- /dev/null +++ b/git/assistkey/commit/1030d2b2c355341a737cc75a6d72dc054e70456f.html @@ -0,0 +1,248 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>assistkey 1030d2b2 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>assistkey</h1><span class="desc">Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</span><p class="url">git clone https://github.com/equwal/assistkey</p><p><a href="../../assistkey/index.html">Log</a> | <a href="../../assistkey/files.html">Files</a> | <a href="../../assistkey/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 1030d2b2c355341a737cc75a6d72dc054e70456f +truex <truex@equwal.com> +2026-09-20 22:33:14 -0700 + +Viwoods: inside the AI screen or the crop screen, the AI key goes back + +The key filter now reads window changes to know which app was in use before an +AI screen opened. It keeps the latest package name only, in memory only. When +the AI key is pressed inside the Viwoods AI assistant, or inside the crop and +screenshot-edit screens, the app brings that earlier app back with the same +intent a launcher sends. If the earlier app was a home screen, it goes Home. + +The firmware reacts to the AI key by itself and can open its AI screen again, +so the return waits 350 ms for that to land first. The same rule applies when +the firmware hook points at AiKeyActivity. + +A switch on the AI key screen turns the rule off. It is on by default on +Viwoods devices and absent on others. + +Test on a device: tools/e2e/ai-key-return.sh. + +</pre><pre> .../dev/equwal/assistkey/channel/AiKeyActivity.kt | 3 + + .../java/dev/equwal/assistkey/device/Device.kt | 17 +++++ + .../equwal/assistkey/engine/KeyFilterService.kt | 76 +++++++++++++++++++++- + .../dev/equwal/assistkey/ui/TriggerListActivity.kt | 8 +++ + tools/e2e/ai-key-return.sh | 36 ++++++++++ + 5 files changed, 139 insertions(+), 1 deletion(-) +</pre><pre>diff --git a/app/src/main/java/dev/equwal/assistkey/channel/AiKeyActivity.kt b/app/src/main/java/dev/equwal/assistkey/channel/AiKeyActivity.kt +index 512d501..8400ee5 100644 +--- a/app/src/main/java/dev/equwal/assistkey/channel/AiKeyActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/channel/AiKeyActivity.kt +<span class="h">@@ -41,6 +41,9 @@ class AiKeyActivity : Activity() {</span> + val svc = ServiceHolder.service as? KeyFilterService + + when { +<span class="a">+ // Inside an AI screen the key means "back to what I was doing".</span> +<span class="a">+ svc != null && svc.inAiScreen && License.active(this) &&</span> +<span class="a">+ dev.equwal.assistkey.device.Device.aiKeyReturns(this) -> svc.returnFromAiScreen()</span> + // Ahead of the licence check: the navigation lifeline lives here. + svc != null && svc.takePowerCombo(HwKey.AI) -> Unit + // Locked means the key behaves as the firmware intended. +diff --git a/app/src/main/java/dev/equwal/assistkey/device/Device.kt b/app/src/main/java/dev/equwal/assistkey/device/Device.kt +index f395775..6091343 100644 +--- a/app/src/main/java/dev/equwal/assistkey/device/Device.kt ++++ b/app/src/main/java/dev/equwal/assistkey/device/Device.kt +<span class="h">@@ -49,6 +49,23 @@ object Device {</span> + val brightnessFloor: Int? get() = if (isViwoods) 5 else null + + private const val PREFS = "assistkey_device" +<span class="a">+ private const val K_AI_RETURNS = "ai_key_returns"</span> +<span class="a">+</span> +<span class="a">+ /**</span> +<span class="a">+ * Viwoods only. Inside the AI screen or the crop screen, the AI key goes</span> +<span class="a">+ * back to the app that was in use before. On by default on that device.</span> +<span class="a">+ */</span> +<span class="a">+ fun aiKeyReturns(c: Context): Boolean =</span> +<span class="a">+ isViwoods && c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(K_AI_RETURNS, true)</span> +<span class="a">+</span> +<span class="a">+ fun setAiKeyReturns(c: Context, on: Boolean) {</span> +<span class="a">+ c.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(K_AI_RETURNS, on).apply()</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ /** True for the Viwoods AI assistant and for the Viwoods crop and screenshot-edit screens. */</span> +<span class="a">+ fun isAiScreen(pkg: String?, cls: String?): Boolean =</span> +<span class="a">+ pkg == "com.viwoods.viwoodsai" ||</span> +<span class="a">+ (pkg == "com.viwoods.launcher" && cls?.contains(".libfloating.") == true)</span> + private const val K_SEEN = "seen_keys" + + private val builtIn: List<HwKey> +diff --git a/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt b/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt +index bd87f80..c77a85b 100644 +--- a/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt ++++ b/app/src/main/java/dev/equwal/assistkey/engine/KeyFilterService.kt +<span class="h">@@ -200,6 +200,7 @@ class KeyFilterService : AccessibilityService(), GestureEngine.Host {</span> + } + val consumed = when { + !Channels.isEnabled(this, Channel.ACCESSIBILITY) -> false +<span class="a">+ key != null && License.active(this) && aiKeyReturn(event, key) -> true</span> + // Ahead of the licence check on purpose - see armPowerCombo. + key != null && key.interceptable && powerCombo(event, key) -> true + // Locked means inert, not broken: every key goes to the firmware. +<span class="h">@@ -222,7 +223,77 @@ class KeyFilterService : AccessibilityService(), GestureEngine.Host {</span> + return consumed + } + +<span class="d">- override fun onAccessibilityEvent(event: AccessibilityEvent?) { /* not used */ }</span> +<span class="a">+ // ---- the AI key, inside an AI screen ------------------------------------------</span> +<span class="a">+</span> +<span class="a">+ /** True while the Viwoods AI screen or crop screen is in front. */</span> +<span class="a">+ var inAiScreen = false</span> +<span class="a">+ private set</span> +<span class="a">+</span> +<span class="a">+ /** The package in front before the AI screen opened. Held in memory only. */</span> +<span class="a">+ private var lastApp: String? = null</span> +<span class="a">+ private val aiSwallowed = HashSet<HwKey>()</span> +<span class="a">+</span> +<span class="a">+ /**</span> +<span class="a">+ * Window changes are watched for one purpose: to know which app to go back</span> +<span class="a">+ * to. Only the package name is kept, only the latest one, and only in</span> +<span class="a">+ * memory.</span> +<span class="a">+ */</span> +<span class="a">+ override fun onAccessibilityEvent(event: AccessibilityEvent?) {</span> +<span class="a">+ if (event?.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) return</span> +<span class="a">+ val pkg = event.packageName?.toString() ?: return</span> +<span class="a">+ val cls = event.className?.toString()</span> +<span class="a">+ when {</span> +<span class="a">+ pkg == packageName || pkg == "android" || pkg == "com.android.systemui" -> Unit</span> +<span class="a">+ dev.equwal.assistkey.device.Device.isAiScreen(pkg, cls) -> inAiScreen = true</span> +<span class="a">+ // Dialogs and keyboards report window changes too; only activities count.</span> +<span class="a">+ cls != null && isActivity(pkg, cls) -> {</span> +<span class="a">+ inAiScreen = false</span> +<span class="a">+ lastApp = pkg</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ private fun isActivity(pkg: String, cls: String): Boolean = runCatching {</span> +<span class="a">+ packageManager.getActivityInfo(android.content.ComponentName(pkg, cls), 0); true</span> +<span class="a">+ }.getOrDefault(false)</span> +<span class="a">+</span> +<span class="a">+ /**</span> +<span class="a">+ * Leaves the AI screen for the app that was in use before it. The firmware</span> +<span class="a">+ * reacts to the AI key by itself and may open its AI screen again, so this</span> +<span class="a">+ * waits for that to land first.</span> +<span class="a">+ */</span> +<span class="a">+ fun returnFromAiScreen() {</span> +<span class="a">+ handler.postDelayed({</span> +<span class="a">+ val pkg = lastApp</span> +<span class="a">+ val launch = pkg?.let { packageManager.getLaunchIntentForPackage(it) }</span> +<span class="a">+ val isHome = pkg != null && packageManager.resolveActivity(</span> +<span class="a">+ android.content.Intent(android.content.Intent.ACTION_MAIN)</span> +<span class="a">+ .addCategory(android.content.Intent.CATEGORY_HOME),</span> +<span class="a">+ 0</span> +<span class="a">+ )?.activityInfo?.packageName == pkg</span> +<span class="a">+ if (launch == null || isHome) {</span> +<span class="a">+ performGlobalAction(GLOBAL_ACTION_HOME)</span> +<span class="a">+ } else {</span> +<span class="a">+ // The same intent the launcher sends: it brings the task back as it was.</span> +<span class="a">+ startActivity(</span> +<span class="a">+ launch.addFlags(</span> +<span class="a">+ android.content.Intent.FLAG_ACTIVITY_NEW_TASK or</span> +<span class="a">+ android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED</span> +<span class="a">+ )</span> +<span class="a">+ )</span> +<span class="a">+ }</span> +<span class="a">+ }, AI_RETURN_DELAY_MS)</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ private fun aiKeyReturn(event: KeyEvent, key: HwKey): Boolean {</span> +<span class="a">+ if (key != HwKey.AI) return false</span> +<span class="a">+ if (event.action == KeyEvent.ACTION_UP) return aiSwallowed.remove(key)</span> +<span class="a">+ if (key in aiSwallowed) return true</span> +<span class="a">+ if (!inAiScreen || !dev.equwal.assistkey.device.Device.aiKeyReturns(this)) return false</span> +<span class="a">+ aiSwallowed.add(key)</span> +<span class="a">+ returnFromAiScreen()</span> +<span class="a">+ return true</span> +<span class="a">+ }</span> + + /** + * The settings screen lives in this same process, so new timing values can +<span class="h">@@ -254,6 +325,9 @@ class KeyFilterService : AccessibilityService(), GestureEngine.Host {</span> + /** How long after a Power hold a key press still counts as a combination. */ + const val POWER_COMBO_WINDOW_MS = 1000L + +<span class="a">+ /** Longer than the firmware takes to open its AI screen on an AI key press. */</span> +<span class="a">+ private const val AI_RETURN_DELAY_MS = 350L</span> +<span class="a">+</span> + /** A press this soon after the screen came on is the press that woke it. */ + private const val WAKE_GRACE_MS = 1000L + +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/TriggerListActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/TriggerListActivity.kt +index 7c9a9ee..df35ef8 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/TriggerListActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/TriggerListActivity.kt +<span class="h">@@ -11,6 +11,7 @@ import dev.equwal.assistkey.model.HwKey</span> + import dev.equwal.assistkey.model.Trigger + import dev.equwal.assistkey.native.ViwoodsBridge + import dev.equwal.assistkey.store.Store +<span class="a">+import dev.equwal.assistkey.ui.Ui.check</span> + import dev.equwal.assistkey.ui.Ui.header + import dev.equwal.assistkey.ui.Ui.note + import dev.equwal.assistkey.ui.Ui.row +<span class="h">@@ -54,6 +55,13 @@ class TriggerListActivity : Activity() {</span> + ) + } + +<span class="a">+ if (keys == setOf(HwKey.AI) && dev.equwal.assistkey.device.Device.isViwoods) {</span> +<span class="a">+ col.check(</span> +<span class="a">+ "In the AI or crop screen, go back",</span> +<span class="a">+ "There the AI key returns to the app you were in, whatever is bound below",</span> +<span class="a">+ dev.equwal.assistkey.device.Device.aiKeyReturns(this)</span> +<span class="a">+ ) { dev.equwal.assistkey.device.Device.setAiKeyReturns(this, it) }</span> +<span class="a">+ }</span> + if (HwKey.AI in keys) { + col.note( + if (ViwoodsBridge.aiHookedToUs(this)) { +diff --git a/tools/e2e/ai-key-return.sh b/tools/e2e/ai-key-return.sh +new file mode 100644 +index 0000000..0a7493c +--- /dev/null ++++ b/tools/e2e/ai-key-return.sh +<span class="h">@@ -0,0 +1,36 @@</span> +<span class="a">+#!/usr/bin/env bash</span> +<span class="a">+# Test on a Viwoods reader: inside the AI screen or the crop screen, the AI key</span> +<span class="a">+# goes back to the app that was in use before.</span> +<span class="a">+#</span> +<span class="a">+# Needs: adb root, release build installed, key remapping on. Presses go through</span> +<span class="a">+# the kernel input node, because `adb shell input keyevent` skips the input</span> +<span class="a">+# filter stage.</span> +<span class="a">+set -u</span> +<span class="a">+export MSYS_NO_PATHCONV=1</span> +<span class="a">+NODE=${AI_NODE:-/dev/input/event5}</span> +<span class="a">+E="sendevent $NODE"; AI="$E 1 59 1; $E 0 0 0; sleep 0.08; $E 1 59 0; $E 0 0 0"</span> +<span class="a">+top() {</span> +<span class="a">+ local t="" i</span> +<span class="a">+ for i in 1 2 3 4 5; do</span> +<span class="a">+ t=$(adb shell "dumpsys activity activities" | grep -m1 topResumedActivity | grep -oE '[A-Za-z0-9_.]+/[A-Za-z0-9_.$]+' | head -1)</span> +<span class="a">+ [ -n "$t" ] && break; sleep 1</span> +<span class="a">+ done; echo "${t%%/*}"; }</span> +<span class="a">+fail=0</span> +<span class="a">+check() { if [ "$2" = "$3" ]; then echo "PASS $1: $2"; else echo "FAIL $1: got '$2', want '$3'"; fail=1; fi; }</span> +<span class="a">+</span> +<span class="a">+adb shell input keyevent KEYCODE_WAKEUP; sleep 2</span> +<span class="a">+adb shell "am start -a android.settings.SETTINGS" >/dev/null 2>&1; sleep 3</span> +<span class="a">+check "start in a normal app" "$(top)" "com.android.settings"</span> +<span class="a">+</span> +<span class="a">+adb shell "am start -n com.viwoods.viwoodsai/com.wisky.wiskyai.WebViewAiActivity" >/dev/null 2>&1; sleep 3</span> +<span class="a">+check "AI screen is in front" "$(top)" "com.viwoods.viwoodsai"</span> +<span class="a">+adb shell "su 0 sh -c '$AI'"; sleep 3</span> +<span class="a">+check "AI key in the AI screen returns to the app" "$(top)" "com.android.settings"</span> +<span class="a">+</span> +<span class="a">+adb shell "su 0 am start -n com.viwoods.launcher/com.viwoods.libfloating.activity.ScreenCaptureActivity" >/dev/null 2>&1; sleep 4</span> +<span class="a">+check "crop screen is in front" "$(top)" "com.viwoods.launcher"</span> +<span class="a">+adb shell "su 0 sh -c '$AI'"; sleep 3</span> +<span class="a">+check "AI key in the crop screen returns to the app" "$(top)" "com.android.settings"</span> +<span class="a">+</span> +<span class="a">+adb shell input keyevent KEYCODE_HOME</span> +<span class="a">+exit $fail</span></pre> +</div></body></html> diff --git a/git/assistkey/commit/14ef6461af9264c077df23e292d93f8e6e0dd3ec.html b/git/assistkey/commit/14ef6461af9264c077df23e292d93f8e6e0dd3ec.html new file mode 100644 index 0000000..1744dcf --- /dev/null +++ b/git/assistkey/commit/14ef6461af9264c077df23e292d93f8e6e0dd3ec.html @@ -0,0 +1,215 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>assistkey 14ef6461 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>assistkey</h1><span class="desc">Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</span><p class="url">git clone https://github.com/equwal/assistkey</p><p><a href="../../assistkey/index.html">Log</a> | <a href="../../assistkey/files.html">Files</a> | <a href="../../assistkey/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 14ef6461af9264c077df23e292d93f8e6e0dd3ec +truex <truex@equwal.com> +2026-09-21 03:06:30 -0700 + +Give the hardware hacks a tile and a screen of their own + +The main screen has a Hardware hacks tile. HacksActivity holds the rows +that were a section of Advanced. The row for the double tap of Power +says that it uses the camera intent and needs no camera button. + +</pre><pre> CHANGELOG.md | 7 +++ + app/src/main/AndroidManifest.xml | 1 + + .../dev/equwal/assistkey/ui/AdvancedActivity.kt | 43 -------------- + .../java/dev/equwal/assistkey/ui/HacksActivity.kt | 69 ++++++++++++++++++++++ + .../java/dev/equwal/assistkey/ui/MainActivity.kt | 3 + + 5 files changed, 80 insertions(+), 43 deletions(-) +</pre><pre>diff --git a/CHANGELOG.md b/CHANGELOG.md +index 890f6e2..899411d 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +<span class="h">@@ -1,5 +1,12 @@</span> + # Changelog + +<span class="a">+## Unreleased</span> +<span class="a">+</span> +<span class="a">+- **Hardware hacks** is a tile on the main screen with a screen of its own:</span> +<span class="a">+ hold Power (assistant role), double tap Power (camera intent), the wallet</span> +<span class="a">+ button, the full Power button (shell access), extra-dim light, and the</span> +<span class="a">+ button settings of the device. Advanced no longer holds them.</span> +<span class="a">+</span> + ## 0.0.11-alpha - 2026-09-21 + + Version code 11. +diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml +index b24282a..1385157 100644 +--- a/app/src/main/AndroidManifest.xml ++++ b/app/src/main/AndroidManifest.xml +<span class="h">@@ -91,6 +91,7 @@</span> + <activity android:name=".ui.SetupActivity" android:exported="false" /> + <activity android:name=".ui.HomeHubActivity" android:exported="false" /> + <activity android:name=".ui.AdvancedActivity" android:exported="false" /> +<span class="a">+ <activity android:name=".ui.HacksActivity" android:exported="false" /></span> + <!-- The guided flow the hub opens first. --> + <activity android:name=".setup.GuidedSetupActivity" android:exported="false" /> + <activity android:name=".menu.MenuEditActivity" android:exported="false" /> +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/AdvancedActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/AdvancedActivity.kt +index 5435f1d..3e80661 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/AdvancedActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/AdvancedActivity.kt +<span class="h">@@ -3,12 +3,9 @@ package dev.equwal.assistkey.ui</span> + import android.app.Activity + import android.content.Intent + import android.widget.LinearLayout +<span class="d">-import dev.equwal.assistkey.channel.Channel</span> +<span class="d">-import dev.equwal.assistkey.channel.Channels</span> + import dev.equwal.assistkey.device.Detect + import dev.equwal.assistkey.device.Device + import dev.equwal.assistkey.model.HwKey +<span class="d">-import dev.equwal.assistkey.native.ViwoodsBridge</span> + import dev.equwal.assistkey.shell.Shell + import dev.equwal.assistkey.store.Store + import dev.equwal.assistkey.ui.Ui.header +<span class="h">@@ -41,7 +38,6 @@ class AdvancedActivity : Activity() {</span> + val col = Ui.page(this, "Advanced") + fullControl(col) + tools(col) +<span class="d">- hacks(col)</span> + settingsFile(col) + } + +<span class="h">@@ -90,45 +86,6 @@ class AdvancedActivity : Activity() {</span> + col.row("Export and import") { startActivity(Intent(this, BackupActivity::class.java)) } + } + +<span class="d">- /**</span> +<span class="d">- * The tricks that get more out of the hardware than Android offers an app.</span> +<span class="d">- * They are in one place, so that a user who wants none of them can pass by.</span> +<span class="d">- */</span> +<span class="d">- private fun hacks(col: LinearLayout) {</span> +<span class="d">- col.header("Hardware hacks")</span> +<span class="d">- fun door(title: String, hint: String, ch: Channel) = col.row(</span> +<span class="d">- title, hint, state = if (Channels.isSatisfied(this, ch)) "On" else "Off"</span> +<span class="d">- ) { startActivity(Intent(this, PowerActivity::class.java)) }</span> +<span class="d">- door("Hold Power", "Rebind stands in for the assistant app", Channel.ASSISTANT)</span> +<span class="d">- door("Double press Power", "Rebind stands in for the camera app", Channel.CAMERA)</span> +<span class="d">- door("Wallet button", "Rebind stands in for the wallet app", Channel.WALLET)</span> +<span class="d">- if (Shell.SUPPORTED) {</span> +<span class="d">- col.row(</span> +<span class="d">- "Full Power button",</span> +<span class="d">- "Every Power press, through shell access",</span> +<span class="d">- state = Shell.describe(this)</span> +<span class="d">- ) { startActivity(Intent(this, ShellActivity::class.java)) }</span> +<span class="d">- // No light node, no extra-dim. Detection says so; do not offer it.</span> +<span class="d">- if (Shell.ready && Detect.stored(this)?.capability(Detect.FRONTLIGHT) != Detect.State.NO) {</span> +<span class="d">- col.row(</span> +<span class="d">- "Extra-dim light",</span> +<span class="d">- "Below the lowest the system slider allows"</span> +<span class="d">- ) { startActivity(Intent(this, DisplayActivity::class.java)) }</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- if (Device.hasFirmwareKeyHooks) {</span> +<span class="d">- val hidden = ViwoodsBridge.keys().filter { ViwoodsBridge.hidesFromFilter(this, it) }</span> +<span class="d">- col.row(</span> +<span class="d">- "Device button settings",</span> +<span class="d">- if (hidden.isEmpty()) "What this device does with each button"</span> +<span class="d">- else "Hiding " + hidden.joinToString(" and ") { it.label.lowercase() } +</span> +<span class="d">- " from this app",</span> +<span class="d">- state = if (hidden.isEmpty()) null else "Hiding a button"</span> +<span class="d">- ) { startActivity(Intent(this, ViwoodsActivity::class.java)) }</span> +<span class="d">- }</span> +<span class="d">-</span> +<span class="d">- }</span> +<span class="d">-</span> + private fun boundOnKeys(): Int { + val keys = Device.keys(this).toSet() + return Store.bindings(this).all().keys.count { t -> +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt +new file mode 100644 +index 0000000..56c7e18 +--- /dev/null ++++ b/app/src/main/java/dev/equwal/assistkey/ui/HacksActivity.kt +<span class="h">@@ -0,0 +1,69 @@</span> +<span class="a">+package dev.equwal.assistkey.ui</span> +<span class="a">+</span> +<span class="a">+import android.app.Activity</span> +<span class="a">+import android.content.Intent</span> +<span class="a">+import dev.equwal.assistkey.channel.Channel</span> +<span class="a">+import dev.equwal.assistkey.channel.Channels</span> +<span class="a">+import dev.equwal.assistkey.device.Detect</span> +<span class="a">+import dev.equwal.assistkey.device.Device</span> +<span class="a">+import dev.equwal.assistkey.native.ViwoodsBridge</span> +<span class="a">+import dev.equwal.assistkey.shell.Shell</span> +<span class="a">+import dev.equwal.assistkey.ui.Ui.row</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * The tricks that get more out of the hardware than Android offers an app:</span> +<span class="a">+ * the app stands in for the assistant, the camera and the wallet to get the</span> +<span class="a">+ * Power button, reads Power through shell access, sets the light below its</span> +<span class="a">+ * floor, and shows what the device itself does with each button.</span> +<span class="a">+ *</span> +<span class="a">+ * They are in one place, so that a user who wants none of them can pass by.</span> +<span class="a">+ */</span> +<span class="a">+class HacksActivity : Activity() {</span> +<span class="a">+</span> +<span class="a">+ private val redraw: () -> Unit = { if (!isFinishing) build() }</span> +<span class="a">+</span> +<span class="a">+ override fun onResume() {</span> +<span class="a">+ super.onResume()</span> +<span class="a">+ Shell.onChange(redraw)</span> +<span class="a">+ build()</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ override fun onPause() {</span> +<span class="a">+ super.onPause()</span> +<span class="a">+ Shell.removeOnChange(redraw)</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ private fun build() {</span> +<span class="a">+ val col = Ui.page(this, "Hardware hacks")</span> +<span class="a">+ fun door(title: String, hint: String, ch: Channel) = col.row(</span> +<span class="a">+ title, hint, state = if (Channels.isSatisfied(this, ch)) "On" else "Off"</span> +<span class="a">+ ) { startActivity(Intent(this, PowerActivity::class.java)) }</span> +<span class="a">+ door("Hold Power", "Through the assistant role", Channel.ASSISTANT)</span> +<span class="a">+ door("Double tap Power", "Through the camera intent. No camera button.", Channel.CAMERA)</span> +<span class="a">+ door("Wallet button", "Through the wallet role", Channel.WALLET)</span> +<span class="a">+ if (Shell.SUPPORTED) {</span> +<span class="a">+ col.row(</span> +<span class="a">+ "Full Power button",</span> +<span class="a">+ "Every Power press, through shell access",</span> +<span class="a">+ state = Shell.describe(this)</span> +<span class="a">+ ) { startActivity(Intent(this, ShellActivity::class.java)) }</span> +<span class="a">+ // No light node, no extra-dim. Detection says so; do not offer it.</span> +<span class="a">+ if (Shell.ready && Detect.stored(this)?.capability(Detect.FRONTLIGHT) != Detect.State.NO) {</span> +<span class="a">+ col.row(</span> +<span class="a">+ "Extra-dim light",</span> +<span class="a">+ "Below the lowest the system slider allows"</span> +<span class="a">+ ) { startActivity(Intent(this, DisplayActivity::class.java)) }</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ if (Device.hasFirmwareKeyHooks) {</span> +<span class="a">+ val hidden = ViwoodsBridge.keys().filter { ViwoodsBridge.hidesFromFilter(this, it) }</span> +<span class="a">+ col.row(</span> +<span class="a">+ "Device button settings",</span> +<span class="a">+ if (hidden.isEmpty()) "What this device does with each button"</span> +<span class="a">+ else "Hiding " + hidden.joinToString(" and ") { it.label.lowercase() } +</span> +<span class="a">+ " from this app",</span> +<span class="a">+ state = if (hidden.isEmpty()) null else "Hiding a button"</span> +<span class="a">+ ) { startActivity(Intent(this, ViwoodsActivity::class.java)) }</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +diff --git a/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt b/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt +index b92adff..80e6162 100644 +--- a/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt ++++ b/app/src/main/java/dev/equwal/assistkey/ui/MainActivity.kt +<span class="h">@@ -143,6 +143,9 @@ class MainActivity : Activity() {</span> + Ui.Tile("Setup", setupSummary()) { + startActivity(Intent(this, SetupActivity::class.java)) + }, +<span class="a">+ Ui.Tile("Hardware hacks", "Power tricks, extra-dim light") {</span> +<span class="a">+ startActivity(Intent(this, HacksActivity::class.java))</span> +<span class="a">+ },</span> + Ui.Tile("Advanced", "Every button, tools and backup") { + startActivity(Intent(this, AdvancedActivity::class.java)) + }</pre> +</div></body></html> diff --git a/git/assistkey/commit/189bad36a4deca54fb94fdddba9c7f460c53f396.html b/git/assistkey/commit/189bad36a4deca54fb94fdddba9c7f460c53f396.html new file mode 100644 index 0000000..2ddd57a --- /dev/null +++ b/git/assistkey/commit/189bad36a4deca54fb94fdddba9c7f460c53f396.html @@ -0,0 +1,155 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>assistkey 189bad36 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>assistkey</h1><span class="desc">Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</span><p class="url">git clone https://github.com/equwal/assistkey</p><p><a href="../../assistkey/index.html">Log</a> | <a href="../../assistkey/files.html">Files</a> | <a href="../../assistkey/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 189bad36a4deca54fb94fdddba9c7f460c53f396 +truex <truex@equwal.com> +2026-09-21 04:05:05 -0700 + +Add the Viwoods AI voice prompt as an action + +On a hold of the AI key, the firmware starts WebViewAiActivity of the +Viwoods AI app with the text extra recodeKey=recode_key_start. The +action AI voice prompt does the same, so that any button can have it. + +The payload of LAUNCH_COMPONENT can carry text extras after a question +mark. ComponentPayload parses it with no Android types. A payload from +before reads as it did. + +</pre><pre> CHANGELOG.md | 5 +++ + .../dev/equwal/assistkey/model/ComponentPayload.kt | 30 ++++++++++++++++++ + .../java/dev/equwal/assistkey/model/Presets.kt | 2 ++ + .../dev/equwal/assistkey/route/ActionRouter.kt | 4 ++- + .../equwal/assistkey/model/ComponentPayloadTest.kt | 37 ++++++++++++++++++++++ + 5 files changed, 77 insertions(+), 1 deletion(-) +</pre><pre>diff --git a/CHANGELOG.md b/CHANGELOG.md +index 3831071..f30fae8 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +<span class="h">@@ -5,6 +5,11 @@</span> + - **Change a button from the Done step.** "Change what it does" and "Change + how you press it" go back to those steps with the button kept. Back on the + Done step goes to the actions too. Tests: `Route.back`. +<span class="a">+- **AI voice prompt** is an action of its own (Viwoods). It is what the</span> +<span class="a">+ firmware does on a hold of the AI key: the Viwoods AI screen, told to start</span> +<span class="a">+ its voice prompt. It needs the Viwoods AI account, as the stock hold does.</span> +<span class="a">+ A component launch can now carry text extras (`pkg/class?key=value`).</span> +<span class="a">+ Tests: `ComponentPayloadTest`.</span> + - **Ask when the user tries it.** The first run asks for every grant once. + After that the app asks at the moment something is missing: after a menu is + saved, after the Power switch on the Navigation screen, when a Power press +diff --git a/app/src/main/java/dev/equwal/assistkey/model/ComponentPayload.kt b/app/src/main/java/dev/equwal/assistkey/model/ComponentPayload.kt +new file mode 100644 +index 0000000..4aea663 +--- /dev/null ++++ b/app/src/main/java/dev/equwal/assistkey/model/ComponentPayload.kt +<span class="h">@@ -0,0 +1,30 @@</span> +<span class="a">+package dev.equwal.assistkey.model</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * The payload of [ActionKind.LAUNCH_COMPONENT]: "pkg/class", and after a "?"</span> +<span class="a">+ * the text extras of the intent as "key=value" with "&" between them.</span> +<span class="a">+ *</span> +<span class="a">+ * The extras exist for a screen that does something else when it gets one.</span> +<span class="a">+ * The Viwoods AI screen starts its voice prompt with recodeKey=recode_key_start.</span> +<span class="a">+ * A payload from before has no "?" and reads as it always did.</span> +<span class="a">+ */</span> +<span class="a">+object ComponentPayload {</span> +<span class="a">+</span> +<span class="a">+ data class Parts(val component: String, val extras: Map<String, String>)</span> +<span class="a">+</span> +<span class="a">+ fun parse(payload: String): Parts {</span> +<span class="a">+ val component = payload.substringBefore('?')</span> +<span class="a">+ val extras = LinkedHashMap<String, String>()</span> +<span class="a">+ if ('?' in payload) {</span> +<span class="a">+ payload.substringAfter('?').split('&').forEach { pair -></span> +<span class="a">+ val key = pair.substringB [diff truncated]