mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f4a6f60c5 | ||
|
|
354e13678b | ||
|
|
f1b8822e20 | ||
|
|
6bf83b1034 | ||
|
|
7b1e4ede2a | ||
|
|
a554820115 | ||
|
|
cb9f9ea5b3 | ||
|
|
4051553a56 | ||
|
|
01a213354b | ||
|
|
962abfb38e | ||
|
|
ada4cf0dfd | ||
|
|
aff1137c3d | ||
|
|
7f24c27201 | ||
|
|
51c5e34220 | ||
|
|
10c2cc64a2 | ||
|
|
a14d21f8e4 | ||
|
|
8de715a153 | ||
|
|
e9ebe832de | ||
|
|
2c8feccea1 | ||
|
|
36083e654f | ||
|
|
8790b1938a | ||
|
|
7cd48ce3c4 | ||
|
|
4300f1005d | ||
|
|
9dec9c370c | ||
|
|
e9f460279a | ||
|
|
06b322e575 | ||
|
|
80d8d8c0cd | ||
|
|
4450db3be9 | ||
|
|
b875489ad3 | ||
|
|
ece3f3822f | ||
|
|
0780cdc80b | ||
|
|
1c0e027abb | ||
|
|
0dcb666bbd | ||
|
|
00ebca7a01 | ||
|
|
f9c8820303 | ||
|
|
3d414edda9 | ||
|
|
0e3f6f1d5e | ||
|
|
c4b0ef38c0 | ||
|
|
2e800bb2de | ||
|
|
505b970497 | ||
|
|
986bfd481c | ||
|
|
ab8d480a01 | ||
|
|
7e19b76403 | ||
|
|
cbc2f103ac | ||
|
|
6a7922cf17 | ||
|
|
8e23de2718 | ||
|
|
11aec5fba1 | ||
|
|
c98f213e2e | ||
|
|
8c11cfcad6 | ||
|
|
5fe88ffc6a | ||
|
|
7a8c7a4ee1 | ||
|
|
3343325195 | ||
|
|
edda16dc06 | ||
|
|
7a3d120123 | ||
|
|
92419c9182 | ||
|
|
68bb5bf371 | ||
|
|
b0d8bfcadd | ||
|
|
a58ad1daf6 | ||
|
|
657c6056c4 | ||
|
|
84daba8902 | ||
|
|
3e37f8a269 | ||
|
|
28d178c4be | ||
|
|
f560cd5930 | ||
|
|
dbf24c6cd3 | ||
|
|
0a4989ca47 | ||
|
|
507dbf5d0f | ||
|
|
536f36f4e7 | ||
|
|
c523ba2287 | ||
|
|
a3f1cbb3b1 | ||
|
|
561bb2f0f4 | ||
|
|
dbbd1b5f2c | ||
|
|
7f6ec2f732 | ||
|
|
e8649203cf | ||
|
|
765b0c3d6d | ||
|
|
248c40731c | ||
|
|
cde01a4863 | ||
|
|
1ee8a0188b | ||
|
|
32e8fc9bf5 | ||
|
|
3c87d895c5 | ||
|
|
82dd8a9b48 | ||
|
|
0996506fd1 | ||
|
|
3ee38ee1e2 | ||
|
|
cf9401d81c | ||
|
|
dbed5cc247 | ||
|
|
625a77fd74 | ||
|
|
f7856cd71a | ||
|
|
3e56c32376 | ||
|
|
56ecad07cd | ||
|
|
de8ef5f10b | ||
|
|
022df97b89 | ||
|
|
662480ef4c | ||
|
|
446743c4cf | ||
|
|
ebb670b328 | ||
|
|
74d253cdc3 | ||
|
|
a368b4a271 | ||
|
|
16a71f4c65 | ||
|
|
3790bca2e7 | ||
|
|
34fd830859 | ||
|
|
9c0be2fe7a | ||
|
|
e5f7f593b2 | ||
|
|
b189a9a435 | ||
|
|
479172f34d | ||
|
|
9eb2a43cd2 | ||
|
|
1110df67d9 | ||
|
|
9a88313199 | ||
|
|
f41560e285 | ||
|
|
4d37a1aca1 | ||
|
|
01fbb0e663 | ||
|
|
91e4bd7475 | ||
|
|
3a111499f1 | ||
|
|
4548981d1b | ||
|
|
d026d26ae9 | ||
|
|
cca7cd7962 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: [jonasbark]
|
||||
open_collective: jonas-bark1
|
||||
custom: ["https://paypal.me/boni"]
|
||||
131
.github/workflows/build.yml
vendored
131
.github/workflows/build.yml
vendored
@@ -4,17 +4,52 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'lib/**'
|
||||
- 'accessibility/**'
|
||||
- 'keypress_simulator/**'
|
||||
- 'pubspec.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Release
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install certificates
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64_MAC }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
# create variables
|
||||
DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_application_certificate.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/pg-signing.keychain-db
|
||||
|
||||
# import certificate and provisioning profile from secrets
|
||||
echo -n "$DEVELOPER_ID_APPLICATION_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH
|
||||
|
||||
# create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# security default-keychain -s $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
# import certificate to keychain
|
||||
security import $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
#2 Setup Java
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v3.12.0
|
||||
@@ -32,16 +67,33 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
#8 Build app ( macos Build )
|
||||
- name: Build App
|
||||
run: flutter build macos --release
|
||||
|
||||
- name: Code Signing
|
||||
run: /usr/bin/codesign --deep --force -s "$DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY" --options runtime SwiftControl.app -v
|
||||
working-directory: build/macos/Build/Products/Release
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }}
|
||||
|
||||
- name: Decode Keystore
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/android.keystore;
|
||||
echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties;
|
||||
|
||||
#6 Building APK
|
||||
- name: Build APK
|
||||
run: flutter build apk --release
|
||||
|
||||
#8 Build app ( macos Build )
|
||||
- name: Build IPA
|
||||
run: flutter build macos --release
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release
|
||||
run: flutter build web --release --base-href "/swiftcontrol/"
|
||||
|
||||
- name: Handle archives
|
||||
run: |
|
||||
cp build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/SwiftControl.android.apk
|
||||
cd build/macos/Build/Products/Release/
|
||||
zip -r SwiftControl.macos.zip SwiftControl.app/
|
||||
|
||||
#9 Upload Artifacts
|
||||
- name: Upload Artifacts
|
||||
@@ -49,8 +101,8 @@ jobs:
|
||||
with:
|
||||
name: Releases
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-release.apk
|
||||
build/macos/Build/Products/Release/SwiftControl.app
|
||||
build/app/outputs/flutter-apk/SwiftControl.android.apk
|
||||
build/macos/Build/Products/Release/SwiftControl.macos.zip
|
||||
|
||||
#10 Extract Version
|
||||
- name: Extract version from pubspec.yaml
|
||||
@@ -81,7 +133,7 @@ jobs:
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "build/app/outputs/flutter-apk/app-release.apk,build/macos/Build/Products/Release/SwiftControl.app"
|
||||
artifacts: "build/app/outputs/flutter-apk/SwiftControl.android.apk,build/macos/Build/Products/Release/SwiftControl.macos.zip"
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
@@ -93,3 +145,66 @@ jobs:
|
||||
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
windows:
|
||||
needs: build
|
||||
name: Build & Release on Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
#2 Setup Java
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v3.12.0
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
java-version: '17'
|
||||
|
||||
#3 Setup Flutter
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
|
||||
#4 Install Dependencies
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build App
|
||||
run: flutter build windows
|
||||
|
||||
- name: Zip directory (Windows)
|
||||
shell: pwsh
|
||||
run: |
|
||||
Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath "build/windows/x64/runner/Release/SwiftControl.windows.zip"
|
||||
|
||||
#9 Upload Artifacts
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Releases
|
||||
path: |
|
||||
build/windows/x64/runner/Release/SwiftControl.windows.zip
|
||||
|
||||
#10 Extract Version
|
||||
- name: Extract version from pubspec.yaml (Windows)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = Select-String '^version: ' pubspec.yaml | ForEach-Object {
|
||||
($_ -split ' ')[1].Trim()
|
||||
}
|
||||
echo "VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
# add artifact to release
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "build/windows/x64/runner/Release/SwiftControl.windows.zip"
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,6 +12,8 @@
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
android/keystore.properties
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
74
CHANGELOG.md
Normal file
74
CHANGELOG.md
Normal file
@@ -0,0 +1,74 @@
|
||||
### 2.0.6 (2025-04-15)
|
||||
- fix MyWhoosh up / downshift button assignment (I key vs K key)
|
||||
|
||||
### 2.0.5 (2025-04-13)
|
||||
- fix Zwift Click button assignment (#12)
|
||||
|
||||
### 2.0.4 (2025-04-10)
|
||||
- vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16)
|
||||
|
||||
### 2.0.3 (2025-04-08)
|
||||
- adjust TrainingPeaks Virtual key mapping (#12)
|
||||
- attempt to reconnect device if connection is lost
|
||||
- Android: detect freeform windows for MyWhoosh + TrainingPeaks Virtual keymaps
|
||||
|
||||
### 2.0.2 (2025-04-07)
|
||||
- fix bluetooth scan issues on older Android devices by asking for location permission
|
||||
|
||||
### 2.0.1 (2025-04-06)
|
||||
- long pressing a button will trigger the action again every 250ms
|
||||
|
||||
### 2.0.0 (2025-04-06)
|
||||
- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices
|
||||
- now shows the battery level of the connected devices
|
||||
- add more troubleshooting information
|
||||
|
||||
### 1.1.10 (2025-04-03)
|
||||
- Add more troubleshooting during connection
|
||||
|
||||
### 1.1.8 (2025-04-02)
|
||||
- Android: make sure the touch reassignment page is fullscreen
|
||||
|
||||
### 1.1.7 (2025-04-01)
|
||||
- Zwift Ride: fix connection issues by connecting only to the left controller
|
||||
- Windows: connect sequentially to fix (finally?) fix connection issues
|
||||
- Windows: change the way keyboard is simulated, should fix glitches
|
||||
|
||||
### 1.1.6 (2025-03-31)
|
||||
- Zwift Ride: add buttonPowerDown to shift gears
|
||||
- Zwift Play: Fix buttonShift assignment
|
||||
- Android: fix action to go to next song
|
||||
- App now checks if you run the latest available version
|
||||
|
||||
### 1.1.5 (2025-03-30)
|
||||
- fix bluetooth connection #6, also add missing entitlement on macOS
|
||||
|
||||
### 1.1.3 (2025-03-30)
|
||||
- Windows: fix custom keyboard profile recreation after restart, also warn when choosing MyWhoosh profile (may fix #7)
|
||||
- Zwift Ride: button map adjustments to prevent double shifting
|
||||
- potential fix for #6
|
||||
|
||||
### 1.1.1 (2025-03-30)
|
||||
- potential fix for Bluetooth device detection
|
||||
|
||||
### 1.1.0 (2025-03-30)
|
||||
- Windows & macOS: allow setting custom keymap and store the setting
|
||||
- Android: allow customizing the touch area, so it can work with any device without guesswork where the buttons are (#4)
|
||||
- Zwift Ride: update Zwift Ride decoding based on Feedback from @JayyajGH (#3)
|
||||
|
||||
### 1.0.6 (2025-03-29)
|
||||
- Another potential keyboard fix for Windows
|
||||
- Zwift Play: actually also use the dedicated shift buttons
|
||||
|
||||
### 1.0.5 (2025-03-29)
|
||||
- Zwift Ride: remap the shifter buttons to the correct values
|
||||
|
||||
### 1.0.0+4 (2025-03-29)
|
||||
- Zwift Ride: attempt to fix button parsing
|
||||
- Android: fix missing permissions
|
||||
- Windows: potential fix for key press issues
|
||||
|
||||
### 1.0.0+3 (2025-03-29)
|
||||
|
||||
- Windows: fix connection by using a different Bluetooth stack (issue #1)
|
||||
- Android: fix non-working touch propagation (issue #2)
|
||||
52
README.md
52
README.md
@@ -2,32 +2,58 @@
|
||||
|
||||
<img src="logo.jpg" alt="SwiftControl Logo"/>
|
||||
|
||||
### Description
|
||||
## Description
|
||||
|
||||
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Primarily useful to perform virtual gear shifting.
|
||||
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
|
||||
- Virtual Gear shifting
|
||||
- Steering / turning
|
||||
- adjust workout intensity
|
||||
- control music on your device
|
||||
- more? If you can do it via keyboard, mouse or touch, you can do it with SwiftControl
|
||||
|
||||
### Supported Apps
|
||||
|
||||
https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
|
||||
|
||||
|
||||
|
||||
## Downloads
|
||||
Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
|
||||
## Supported Apps
|
||||
- MyWhoosh
|
||||
- indieVelo / Training Peaks
|
||||
- let me know if you know others that can benefit
|
||||
- any other:
|
||||
- Android: you can customize simulated touch points of all your buttons in the app
|
||||
- Desktop: you can customize keyboard shortcuts and mouse clicks in the app
|
||||
|
||||
### Supported Devices
|
||||
## Supported Devices
|
||||
- Zwift Click
|
||||
- Zwift Ride
|
||||
- Zwift Play
|
||||
|
||||
### Supported Platforms
|
||||
## Supported Platforms
|
||||
- Android
|
||||
- macOS
|
||||
- Windows
|
||||
- Windows
|
||||
- make sure you have installed the "[Microsoft Visual C++ Runtime libraries](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)"
|
||||
- Windows may flag the app as virus. I think it does so because the app does control the mouse and keyboard.
|
||||
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
|
||||
|
||||
### How does it work?
|
||||
## Troubleshooting
|
||||
Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
|
||||
|
||||
## How does it work?
|
||||
The app connects to your Zwift device automatically.
|
||||
|
||||
- When using Android a "click" on a certain part of the screen is simulated to trigger the action.
|
||||
- When using macOS or Windows a keyboard click is used to trigger the action. Typically + and - keys are used to shift gears, while MyWhoosh uses K and I keys.
|
||||
- When using macOS or Windows a keyboard or mouse click is used to trigger the action.
|
||||
- there are predefined Keymaps for MyWhoosh and indieVelo / Training Peaks
|
||||
- you can also create your own Keymaps for any other app
|
||||
- you can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts
|
||||
|
||||
## Donate
|
||||
Please consider donating to support the development of this app :)
|
||||
|
||||
[](https://paypal.me/boni)
|
||||
|
||||
### TODO
|
||||
- test Zwift Ride
|
||||
- implement more actions for Play + Ride
|
||||
- shorebird?
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.common.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@@ -45,26 +40,45 @@ class FlutterError (
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
|
||||
enum class MediaAction(val raw: Int) {
|
||||
PLAY_PAUSE(0),
|
||||
NEXT(1),
|
||||
VOLUME_UP(2),
|
||||
VOLUME_DOWN(3);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): MediaAction? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class WindowEvent (
|
||||
val packageName: String,
|
||||
val windowHeight: Long,
|
||||
val windowWidth: Long
|
||||
val top: Long,
|
||||
val bottom: Long,
|
||||
val right: Long,
|
||||
val left: Long
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): WindowEvent {
|
||||
val packageName = pigeonVar_list[0] as String
|
||||
val windowHeight = pigeonVar_list[1] as Long
|
||||
val windowWidth = pigeonVar_list[2] as Long
|
||||
return WindowEvent(packageName, windowHeight, windowWidth)
|
||||
val top = pigeonVar_list[1] as Long
|
||||
val bottom = pigeonVar_list[2] as Long
|
||||
val right = pigeonVar_list[3] as Long
|
||||
val left = pigeonVar_list[4] as Long
|
||||
return WindowEvent(packageName, top, bottom, right, left)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
packageName,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
left,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -75,8 +89,10 @@ data class WindowEvent (
|
||||
return true
|
||||
}
|
||||
return packageName == other.packageName
|
||||
&& windowHeight == other.windowHeight
|
||||
&& windowWidth == other.windowWidth
|
||||
&& top == other.top
|
||||
&& bottom == other.bottom
|
||||
&& right == other.right
|
||||
&& left == other.left
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
@@ -85,6 +101,11 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
MediaAction.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
WindowEvent.fromList(it)
|
||||
}
|
||||
@@ -94,8 +115,12 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() {
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is WindowEvent -> {
|
||||
is MediaAction -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is WindowEvent -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
@@ -110,6 +135,7 @@ interface Accessibility {
|
||||
fun hasPermission(): Boolean
|
||||
fun openPermissions()
|
||||
fun performTouch(x: Double, y: Double)
|
||||
fun controlMedia(action: MediaAction)
|
||||
|
||||
companion object {
|
||||
/** The codec used by Accessibility. */
|
||||
@@ -170,6 +196,24 @@ interface Accessibility {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.controlMedia$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val actionArg = args[0] as MediaAction
|
||||
val wrapped: List<Any?> = try {
|
||||
api.controlMedia(actionArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,9 +235,9 @@ private class AccessibilityApiPigeonStreamHandler<T>(
|
||||
}
|
||||
|
||||
interface AccessibilityApiPigeonEventChannelWrapper<T> {
|
||||
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
|
||||
fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
|
||||
|
||||
open fun onCancel(p0: Any?) {}
|
||||
fun onCancel(p0: Any?) {}
|
||||
}
|
||||
|
||||
class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
|
||||
@@ -209,7 +253,7 @@ class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
|
||||
sink.endOfStream()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWrapper<WindowEvent> {
|
||||
companion object {
|
||||
fun register(messenger: BinaryMessenger, streamHandler: StreamEventsStreamHandler, instanceName: String = "") {
|
||||
@@ -222,4 +266,4 @@ abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWra
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
package de.jonasbark.accessibility
|
||||
|
||||
import Accessibility
|
||||
import MediaAction
|
||||
import PigeonEventSink
|
||||
import StreamEventsStreamHandler
|
||||
import WindowEvent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
|
||||
/** AccessibilityPlugin */
|
||||
class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
class AccessibilityPlugin: FlutterPlugin, Accessibility {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
@@ -37,14 +36,6 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
Observable.fromService = eventHandler
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
if (call.method == "getPlatformVersion") {
|
||||
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
@@ -64,6 +55,22 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
Observable.toService?.performTouch(x = x, y = y) ?: error("Service not running")
|
||||
}
|
||||
|
||||
override fun controlMedia(action: MediaAction) {
|
||||
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
|
||||
when (action) {
|
||||
MediaAction.PLAY_PAUSE -> {
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
}
|
||||
MediaAction.NEXT -> {
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
}
|
||||
MediaAction.VOLUME_DOWN -> audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
MediaAction.VOLUME_UP -> audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EventListener : StreamEventsStreamHandler(), Receiver {
|
||||
@@ -78,8 +85,8 @@ class EventListener : StreamEventsStreamHandler(), Receiver {
|
||||
eventSink = null
|
||||
}
|
||||
|
||||
override fun onChange(packageName: String, windowWidth: Int, windowHeight: Int) {
|
||||
eventSink?.success(WindowEvent(packageName = packageName, windowWidth = windowWidth.toLong(), windowHeight = windowHeight.toLong()))
|
||||
override fun onChange(packageName: String, window: Rect) {
|
||||
eventSink?.success(WindowEvent(packageName = packageName, right = window.right.toLong(), left = window.left.toLong(), bottom = window.bottom.toLong(), top = window.top.toLong()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ import android.accessibilityservice.GestureDescription
|
||||
import android.accessibilityservice.GestureDescription.StrokeDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED
|
||||
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||
|
||||
|
||||
@@ -26,26 +24,24 @@ class AccessibilityService : AccessibilityService(), Listener {
|
||||
Observable.toService = null
|
||||
}
|
||||
|
||||
private val ignorePackages = listOf("com.android.systemui", "com.android.launcher", "com.android.settings")
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
if (event.packageName == null || rootInActiveWindow == null) {
|
||||
return
|
||||
}
|
||||
if (event.contentChangeTypes == CONTENT_CHANGE_TYPE_PANE_DISAPPEARED) {
|
||||
if (event.eventType != TYPE_WINDOW_STATE_CHANGED || event.packageName in ignorePackages) {
|
||||
// we're not interested
|
||||
return
|
||||
}
|
||||
val currentPackageName = event.packageName.toString()
|
||||
val windowSize = getWindowSize()
|
||||
Observable.fromService?.onChange(packageName = currentPackageName, windowHeight = windowSize.bottom, windowWidth = windowSize.right)
|
||||
Observable.fromService?.onChange(packageName = currentPackageName, window = windowSize)
|
||||
}
|
||||
|
||||
private fun getWindowSize(): Rect {
|
||||
val outBounds = Rect()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
rootInActiveWindow.getBoundsInWindow(outBounds)
|
||||
} else {
|
||||
rootInActiveWindow.getBoundsInScreen(outBounds)
|
||||
}
|
||||
rootInActiveWindow.getBoundsInScreen(outBounds)
|
||||
return outBounds
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.jonasbark.accessibility
|
||||
|
||||
import android.graphics.Rect
|
||||
|
||||
object Observable {
|
||||
var toService: Listener? = null
|
||||
var fromService: Receiver? = null
|
||||
@@ -10,5 +12,5 @@ interface Listener {
|
||||
}
|
||||
|
||||
interface Receiver {
|
||||
fun onChange(packageName: String, windowWidth: Int, windowHeight: Int)
|
||||
fun onChange(packageName: String, window: Rect)
|
||||
}
|
||||
@@ -7,14 +7,26 @@ abstract class Accessibility {
|
||||
void openPermissions();
|
||||
|
||||
void performTouch(double x, double y);
|
||||
|
||||
void controlMedia(MediaAction action);
|
||||
}
|
||||
|
||||
enum MediaAction { playPause, next, volumeUp, volumeDown }
|
||||
|
||||
class WindowEvent {
|
||||
final String packageName;
|
||||
final int windowHeight;
|
||||
final int windowWidth;
|
||||
final int top;
|
||||
final int bottom;
|
||||
final int right;
|
||||
final int left;
|
||||
|
||||
WindowEvent({required this.packageName, required this.windowHeight, required this.windowWidth});
|
||||
WindowEvent({
|
||||
required this.packageName,
|
||||
required this.left,
|
||||
required this.right,
|
||||
required this.top,
|
||||
required this.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
@EventChannelApi()
|
||||
|
||||
@@ -15,24 +15,39 @@ PlatformException _createConnectionError(String channelName) {
|
||||
);
|
||||
}
|
||||
|
||||
enum MediaAction {
|
||||
playPause,
|
||||
next,
|
||||
volumeUp,
|
||||
volumeDown,
|
||||
}
|
||||
|
||||
class WindowEvent {
|
||||
WindowEvent({
|
||||
required this.packageName,
|
||||
required this.windowHeight,
|
||||
required this.windowWidth,
|
||||
required this.top,
|
||||
required this.bottom,
|
||||
required this.right,
|
||||
required this.left,
|
||||
});
|
||||
|
||||
String packageName;
|
||||
|
||||
int windowHeight;
|
||||
int top;
|
||||
|
||||
int windowWidth;
|
||||
int bottom;
|
||||
|
||||
int right;
|
||||
|
||||
int left;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
packageName,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
left,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,8 +58,10 @@ class WindowEvent {
|
||||
result as List<Object?>;
|
||||
return WindowEvent(
|
||||
packageName: result[0]! as String,
|
||||
windowHeight: result[1]! as int,
|
||||
windowWidth: result[2]! as int,
|
||||
top: result[1]! as int,
|
||||
bottom: result[2]! as int,
|
||||
right: result[3]! as int,
|
||||
left: result[4]! as int,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,8 +76,10 @@ class WindowEvent {
|
||||
}
|
||||
return
|
||||
packageName == other.packageName
|
||||
&& windowHeight == other.windowHeight
|
||||
&& windowWidth == other.windowWidth;
|
||||
&& top == other.top
|
||||
&& bottom == other.bottom
|
||||
&& right == other.right
|
||||
&& left == other.left;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -77,8 +96,11 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is WindowEvent) {
|
||||
} else if (value is MediaAction) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is WindowEvent) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
@@ -89,6 +111,9 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : MediaAction.values[value];
|
||||
case 130:
|
||||
return WindowEvent.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@@ -184,6 +209,29 @@ class Accessibility {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> controlMedia(MediaAction action) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.controlMedia$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[action]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream<WindowEvent> streamEvents( {String instanceName = ''}) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.*
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -5,6 +8,11 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
namespace = "de.jonasbark.swift_play"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -33,11 +41,18 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("config") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = file("../${keystoreProperties["storeFile"] as String}")
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- to check if you have the latest version -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="SwiftControl"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: "fix: "
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps To Reproduce**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional Context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Build System
|
||||
about: Changes that affect the build system or external dependencies
|
||||
title: "build: "
|
||||
labels: build
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Describe what changes need to be done to the build system and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] The build system is passing
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Chore
|
||||
about: Other changes that don't modify src or test files
|
||||
title: "chore: "
|
||||
labels: chore
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what change is needed and why. If this changes code then please use another issue type.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] No functional changes to the code
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Continuous Integration
|
||||
about: Changes to the CI configuration files and scripts
|
||||
title: "ci: "
|
||||
labels: ci
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Describe what changes need to be done to the ci/cd system and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] The ci system is passing
|
||||
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Documentation
|
||||
about: Improve the documentation so all collaborators have a common understanding
|
||||
title: "docs: "
|
||||
labels: documentation
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what documentation you are looking to add or improve.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] Requirements go here
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: A new feature to be added to the project
|
||||
title: "feat: "
|
||||
labels: feature
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what you are looking to add. The more context the better.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] Checklist of requirements to be fulfilled
|
||||
|
||||
**Additional Context**
|
||||
|
||||
Add any other context or screenshots about the feature request go here.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Performance Update
|
||||
about: A code change that improves performance
|
||||
title: "perf: "
|
||||
labels: performance
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Refactor
|
||||
about: A code change that neither fixes a bug nor adds a feature
|
||||
title: "refactor: "
|
||||
labels: refactor
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Revert Commit
|
||||
about: Reverts a previous commit
|
||||
title: "revert: "
|
||||
labels: revert
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Provide a link to a PR/Commit that you are looking to revert and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] Change has been reverted
|
||||
- [ ] No change in test coverage has happened
|
||||
- [ ] A new ticket is created for any follow on work that needs to happen
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Style Changes
|
||||
about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
|
||||
title: "style: "
|
||||
labels: style
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what you are looking to change and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Test
|
||||
about: Adding missing tests or correcting existing tests
|
||||
title: "test: "
|
||||
labels: test
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,27 +0,0 @@
|
||||
<!--
|
||||
Thanks for contributing!
|
||||
|
||||
Provide a description of your changes below and a general summary in the title
|
||||
|
||||
Please look at the following checklist to ensure that your PR can be accepted quickly:
|
||||
-->
|
||||
|
||||
## Status
|
||||
|
||||
**READY/IN DEVELOPMENT/HOLD**
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
<!--- Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] 🧹 Code refactor
|
||||
- [ ] ✅ Build configuration change
|
||||
- [ ] 📝 Documentation
|
||||
- [ ] 🗑️ Chore
|
||||
21
flutter_blue_plus_windows/.github/cspell.json
vendored
21
flutter_blue_plus_windows/.github/cspell.json
vendored
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||
"dictionaries": ["vgv_allowed", "vgv_forbidden"],
|
||||
"dictionaryDefinitions": [
|
||||
{
|
||||
"name": "vgv_allowed",
|
||||
"path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt",
|
||||
"description": "Allowed VGV Spellings"
|
||||
},
|
||||
{
|
||||
"name": "vgv_forbidden",
|
||||
"path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt",
|
||||
"description": "Forbidden VGV Spellings"
|
||||
}
|
||||
],
|
||||
"useGitignore": true,
|
||||
"words": [
|
||||
"flutter_blue_plus_windows"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
version: 2
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
12
flutter_blue_plus_windows/.gitignore
vendored
12
flutter_blue_plus_windows/.gitignore
vendored
@@ -1,12 +0,0 @@
|
||||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
pubspec.lock
|
||||
/.idea/
|
||||
/.flutter-plugins
|
||||
/.flutter-plugins-dependencies
|
||||
|
||||
interanl_example
|
||||
@@ -1,106 +0,0 @@
|
||||
## 1.26.1
|
||||
* Add setOptions.
|
||||
|
||||
## 1.26.0
|
||||
* Set `flutter_blue_plus` upperbound <1.35.0 (due to api changes)
|
||||
|
||||
## 1.25.0
|
||||
* Ensure tracing connection when reconnection occurs after force disconnection.
|
||||
|
||||
## 1.24.22
|
||||
* Upgrade FBP version to `>=1.32.4 <=1.40.0` #24.
|
||||
|
||||
## 1.24.21
|
||||
* fix: startScan() doesn't return correct ScanResult #25
|
||||
|
||||
## 1.24.20
|
||||
* Downgrade FBP version to `>=1.32.4 <=1.33.6` due to the breaking changes.
|
||||
* After upgrade process, the dependencies will be returned to `>=1.34.4 <1.40.0` #24.
|
||||
|
||||
## 1.24.19
|
||||
* Fix a bug with `onValueReceived` of emitting write packet #22.
|
||||
|
||||
## 1.24.18
|
||||
* Add implementation of `BluetoothDeviceWindow.fromId()` #21.
|
||||
|
||||
## 1.24.15
|
||||
* Fix a bug w.r.t. company ID in manufacturer data. (@betto-a #18)
|
||||
|
||||
## 1.24.14
|
||||
* Implement cancelOnDisconnect (@jefflongo #16)
|
||||
|
||||
## 1.24.12
|
||||
* Fix minor bug w.r.t. `characteristic.isNotifying`.
|
||||
|
||||
## 1.24.11
|
||||
* Fix breaking changes of FBP w.r.t. `systemDevices(List withServices)`.
|
||||
|
||||
## 1.24.10
|
||||
* Add support for `cancelWhenScanComplete`
|
||||
|
||||
## 1.24.9
|
||||
* Implement scan filter (including `withServices`, `withRemoteIds`, `withNames`).
|
||||
|
||||
## 1.24.8
|
||||
* Keep manufacturer data when scanning.
|
||||
|
||||
## 1.24.7
|
||||
* Keep service uuids when scanning.
|
||||
|
||||
## 1.24.0
|
||||
* Update `README.md`.
|
||||
|
||||
## 1.23.6
|
||||
* Add unimplemented notification for `read` or `write`.
|
||||
|
||||
## 1.14.0
|
||||
* Remove dependencies `ffi` and `win32` to avoid compile error for web
|
||||
|
||||
## 1.9.5
|
||||
* Apply `flutter blue plus` to `1.28.13`.
|
||||
|
||||
## 1.9.0
|
||||
* Apply a breaking changes `Guid` in `Flutter blue plus` packages.
|
||||
* Use `uuid128` instead of `toString()`.
|
||||
|
||||
## 1.8.10
|
||||
* Fix `Guid` bug related with `Flutter blue plus` packages.
|
||||
|
||||
## 1.8.0
|
||||
* Fix bug with Guid converted from string due to starting/ending with '{ }' in `WinBLE`
|
||||
|
||||
## 1.7.0
|
||||
* Apply `flutter blue plus 1.28.5` (there is several breaking changes.).
|
||||
|
||||
## 1.6.6
|
||||
* Add cache for storing characteristics.
|
||||
|
||||
## 1.6.0
|
||||
* Apply `Flutter blue plus 1.26.0`, (there is a breaking change with `connect()`).
|
||||
|
||||
## 1.5.7
|
||||
* Remove connection by OS when performing `startScan`.
|
||||
|
||||
## 1.5.3
|
||||
* Write logs when connection state stream is started/terminated.
|
||||
|
||||
## 1.5.2
|
||||
* Fix a bug of features added in `1.5.1`
|
||||
|
||||
## 1.5.1
|
||||
* Remove device from connected device list when device is disconnected.
|
||||
|
||||
## 1.5.0
|
||||
* Split functionality of `disconnect` / `removeBond`.
|
||||
|
||||
## 1.4.0
|
||||
* Implement `Subscribe/Unsubscribe Characteristic`.
|
||||
|
||||
## 1.1.0
|
||||
* Implement `Read/Write Characteristic`.
|
||||
|
||||
## 1.0.5
|
||||
* Change `rxdart` version to `0.27.7`.
|
||||
|
||||
## 1.0.0
|
||||
* Initial release (using Github action).
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright 2023 Himchan Park
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,54 +0,0 @@
|
||||
[](https://pub.dartlang.org/packages/flutter_blue_plus_windows)
|
||||
|
||||
## Flutter Blue Plus Windows
|
||||
|
||||
This project is a wrapper library for `Flutter Blue Plus` and `Win_ble`.
|
||||
It allows `Flutter_blue_plus` to operate on Windows.
|
||||
|
||||
With minimal effort, you can use Flutter Blue Plus on Windows.
|
||||
|
||||
## Usage
|
||||
Only you need to do is change the import statement.
|
||||
|
||||
```dart
|
||||
// instead of import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
// Alternatively, you can hide FlutterBluePlus when importing the FBP statement
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus;
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
```
|
||||
|
||||
### Scan devices
|
||||
```dart
|
||||
final scannedDevices = <ScanResult>{};
|
||||
|
||||
const timeout = Duration(seconds: 3);
|
||||
FlutterBluePlus.startScan(timeout: timeout);
|
||||
|
||||
final sub = FlutterBluePlus.scanResults.expand((e)=>e).listen(scannedDevices.add);
|
||||
|
||||
await Future.delayed(timeout);
|
||||
sub.cancel();
|
||||
scannedDevices.forEach(print);
|
||||
```
|
||||
|
||||
### Connect a device
|
||||
```dart
|
||||
final scannedDevice = scannedDevices
|
||||
.where((scanResult) => scanResult.device.platformName == DEVICE_NAME)
|
||||
.firstOrNull;
|
||||
final device = scannedDevice?.device;
|
||||
device?.connect();
|
||||
```
|
||||
|
||||
### Disconnect the device
|
||||
```dart
|
||||
device?.disconnect();
|
||||
```
|
||||
|
||||
Check out the usage of Flutter Blue Plus on [Flutter Blue Plus](https://pub.dev/packages/flutter_blue_plus)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
library flutter_blue_plus_windows;
|
||||
|
||||
export 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus;
|
||||
export 'package:win_ble/win_ble.dart';
|
||||
export 'package:win_ble/win_file.dart';
|
||||
|
||||
export 'src/flutter_blue_plus_windows.dart';
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:win_ble/win_ble.dart';
|
||||
|
||||
extension BluetoothAdapterStateExtension on BleState {
|
||||
BluetoothAdapterState toAdapterState() {
|
||||
switch(this){
|
||||
case BleState.On:
|
||||
return BluetoothAdapterState.on;
|
||||
case BleState.Off:
|
||||
return BluetoothAdapterState.off;
|
||||
case BleState.Unknown:
|
||||
return BluetoothAdapterState.unknown;
|
||||
case BleState.Disabled:
|
||||
return BluetoothAdapterState.unavailable;
|
||||
case BleState.Unsupported:
|
||||
return BluetoothAdapterState.unauthorized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
extension BluetoothCharacteristicExtension on BluetoothCharacteristic {
|
||||
BmBluetoothCharacteristic toProto() {
|
||||
return BmBluetoothCharacteristic(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristicUuid: characteristicUuid,
|
||||
descriptors: [for (final d in descriptors) d.toProto()],
|
||||
properties: properties.toProto(),
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
|
||||
extension BluetoothDescriptorExtension on BluetoothDescriptor {
|
||||
BmBluetoothDescriptor toProto() {
|
||||
return BmBluetoothDescriptor(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristicUuid: characteristicUuid,
|
||||
descriptorUuid: descriptorUuid,
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
extension BluetoothServiceExtension on BluetoothService {
|
||||
BmBluetoothService toProto() {
|
||||
return BmBluetoothService(
|
||||
serviceUuid: serviceUuid,
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
characteristics: [for (final c in characteristics) c.toProto()],
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
|
||||
extension CharacteristicPropertiesExtension on CharacteristicProperties {
|
||||
BmCharacteristicProperties toProto() {
|
||||
return BmCharacteristicProperties(
|
||||
broadcast: broadcast,
|
||||
read: read,
|
||||
writeWithoutResponse: writeWithoutResponse,
|
||||
write: write,
|
||||
notify: notify,
|
||||
indicate: indicate,
|
||||
authenticatedSignedWrites: authenticatedSignedWrites,
|
||||
extendedProperties: extendedProperties,
|
||||
notifyEncryptionRequired: notifyEncryptionRequired,
|
||||
indicateEncryptionRequired: indicateEncryptionRequired,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export 'bluetooth_adapter_state_extension.dart';
|
||||
export 'bluetooth_characteristic_extension.dart';
|
||||
export 'bluetooth_descriptor_extension.dart';
|
||||
export 'bluetooth_service_extension.dart';
|
||||
export 'characteristic_properties_extension.dart';
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'extension/extension.dart';
|
||||
export 'windows/windows.dart';
|
||||
export 'wrapper/wrapper.dart';
|
||||
@@ -1,160 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
class BluetoothCharacteristicWindows extends BluetoothCharacteristic {
|
||||
final DeviceIdentifier remoteId;
|
||||
final Guid serviceUuid;
|
||||
final Guid? secondaryServiceUuid;
|
||||
final Guid characteristicUuid;
|
||||
final List<BluetoothDescriptor> descriptors;
|
||||
|
||||
final Properties propertiesWinBle;
|
||||
|
||||
BluetoothCharacteristicWindows({
|
||||
required this.remoteId,
|
||||
required this.serviceUuid,
|
||||
required this.characteristicUuid,
|
||||
required this.descriptors,
|
||||
required this.propertiesWinBle,
|
||||
this.secondaryServiceUuid,
|
||||
}) : super.fromProto(
|
||||
BmBluetoothCharacteristic(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristicUuid: characteristicUuid,
|
||||
descriptors: [
|
||||
for (final descriptor in descriptors)
|
||||
BmBluetoothDescriptor(
|
||||
remoteId: DeviceIdentifier(descriptor.remoteId.str),
|
||||
serviceUuid: descriptor.serviceUuid,
|
||||
characteristicUuid: descriptor.characteristicUuid,
|
||||
descriptorUuid: descriptor.uuid,
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
),
|
||||
],
|
||||
properties: BmCharacteristicProperties(
|
||||
broadcast: propertiesWinBle.broadcast ?? false,
|
||||
read: propertiesWinBle.read ?? false,
|
||||
writeWithoutResponse: propertiesWinBle.writeWithoutResponse ?? false,
|
||||
write: propertiesWinBle.write ?? false,
|
||||
notify: propertiesWinBle.notify ?? false,
|
||||
indicate: propertiesWinBle.indicate ?? false,
|
||||
authenticatedSignedWrites: propertiesWinBle.authenticatedSignedWrites ?? false,
|
||||
// TODO: implementation missing
|
||||
extendedProperties: false,
|
||||
// TODO: implementation missing
|
||||
notifyEncryptionRequired: false,
|
||||
// TODO: implementation missing
|
||||
indicateEncryptionRequired: false,
|
||||
),
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
),
|
||||
);
|
||||
|
||||
String get _address => remoteId.str.toLowerCase();
|
||||
|
||||
String get _key => "$serviceUuid:$characteristicUuid";
|
||||
|
||||
FBP.BluetoothDevice get device =>
|
||||
FlutterBluePlusWindows.connectedDevices.firstWhere((device) => device.remoteId == remoteId);
|
||||
|
||||
/// this variable is updated:
|
||||
/// - anytime `read()` is called
|
||||
/// - anytime `write()` is called
|
||||
/// - anytime a notification arrives (if subscribed)
|
||||
List<int> get lastValue => FlutterBluePlusWindows._lastChrs[remoteId]?[_key] ?? [];
|
||||
|
||||
/// this stream emits values:
|
||||
/// - anytime `read()` is called
|
||||
/// - anytime `write()` is called
|
||||
/// - anytime a notification arrives (if subscribed)
|
||||
/// - and when first listened to, it re-emits the last value for convenience
|
||||
Stream<List<int>> get lastValueStream => _mergeStreams(
|
||||
[
|
||||
WinBle.characteristicValueStreamOf(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
),
|
||||
FlutterBluePlusWindows._charReadWriteStream.where((e) => e.$1 == _key).map((e) => e.$2)
|
||||
],
|
||||
).map((p) => <int>[...p]).newStreamWithInitialValue(lastValue).asBroadcastStream();
|
||||
|
||||
/// this stream emits values:
|
||||
/// - anytime `read()` is called
|
||||
/// - anytime a notification arrives (if subscribed)
|
||||
Stream<List<int>> get onValueReceived => _mergeStreams(
|
||||
[
|
||||
WinBle.characteristicValueStreamOf(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
),
|
||||
FlutterBluePlusWindows._charReadStream.where((e) => e.$1 == _key).map((e) => e.$2)
|
||||
],
|
||||
).map((p) => <int>[...p]).asBroadcastStream();
|
||||
|
||||
// TODO: need to verify
|
||||
bool get isNotifying => FlutterBluePlusWindows._isNotifying[remoteId]?[_key] ?? false;
|
||||
|
||||
Future<List<int>> read({int timeout = 15}) async {
|
||||
final value = await WinBle.read(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
);
|
||||
FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value));
|
||||
FlutterBluePlusWindows._charReadStreamController.add((_key, value));
|
||||
FlutterBluePlusWindows._lastChrs[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<void> write(List<int> value,
|
||||
{bool allowLongWrite = false, bool withoutResponse = false, int timeout = 15}) async {
|
||||
await WinBle.write(
|
||||
address: _address,
|
||||
service: serviceUuid.str128,
|
||||
characteristic: characteristicUuid.str128,
|
||||
data: Uint8List.fromList(value),
|
||||
writeWithResponse: !withoutResponse, // propertiesWinBle.writeWithoutResponse ?? false,
|
||||
);
|
||||
FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value));
|
||||
FlutterBluePlusWindows._lastChrs[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value;
|
||||
}
|
||||
|
||||
// TODO: need to verify
|
||||
Future<bool> setNotifyValue(
|
||||
bool notify, {
|
||||
int timeout = 15, // TODO: missing implementation
|
||||
bool forceIndications = false, // TODO: missing implementation
|
||||
}) async {
|
||||
/// unSubscribeFromCharacteristic
|
||||
try {
|
||||
await WinBle.unSubscribeFromCharacteristic(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
);
|
||||
} catch (e) {
|
||||
log('WinBle.unSubscribeFromCharacteristic was performed '
|
||||
'before setNotifyValue()');
|
||||
}
|
||||
|
||||
/// set notify
|
||||
try {
|
||||
if (notify) {
|
||||
await WinBle.subscribeToCharacteristic(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
);
|
||||
}
|
||||
FlutterBluePlusWindows._isNotifying[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._isNotifying[remoteId]?[_key] = notify;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
// Bluetooth Device Page:
|
||||
// https://github.com/boskokg/flutter_blue_plus/blob/master/lib/src/bluetooth_device.dart
|
||||
|
||||
part of 'windows.dart';
|
||||
|
||||
class BluetoothDeviceWindows extends FBP.BluetoothDevice {
|
||||
BluetoothDeviceWindows({required super.remoteId});
|
||||
|
||||
// used for 'servicesStream' public api
|
||||
final _services = StreamController<List<BluetoothServiceWindows>>.broadcast();
|
||||
|
||||
// used for 'isDiscoveringServices' public api
|
||||
final _isDiscoveringServices = _StreamController(initialValue: false);
|
||||
|
||||
String get _address => remoteId.str.toLowerCase();
|
||||
|
||||
/// Create a device from an id
|
||||
/// - to connect, this device must have been discovered by your app in a previous scan
|
||||
/// - iOS uses 128-bit uuids the remoteId, e.g. e006b3a7-ef7b-4980-a668-1f8005f84383
|
||||
/// - Android uses 48-bit mac addresses as the remoteId, e.g. 06:E5:28:3B:FD:E0
|
||||
static FBP.BluetoothDevice fromId(String remoteId) {
|
||||
if (Platform.isWindows) {
|
||||
return BluetoothDeviceWindows(remoteId: DeviceIdentifier(remoteId.toUpperCase()));
|
||||
}
|
||||
return FBP.BluetoothDevice.fromId(remoteId);
|
||||
}
|
||||
|
||||
/// platform name
|
||||
/// - this name is kept track of by the platform
|
||||
/// - this name usually persist between app restarts
|
||||
/// - iOS: after you connect, iOS uses the GAP name characteristic (0x2A00)
|
||||
/// if it exists. Otherwise iOS use the advertised name.
|
||||
/// - Android: always uses the advertised name
|
||||
String get platformName => FlutterBluePlusWindows._platformNames[remoteId] ?? "";
|
||||
|
||||
/// Advertised Named
|
||||
/// - this is the name advertised by the device during scanning
|
||||
/// - it is only available after you scan with FlutterBluePlus
|
||||
/// - it is cleared when the app restarts.
|
||||
/// - not all devices advertise a name
|
||||
String get advName => FlutterBluePlusWindows._advNames[remoteId] ?? "";
|
||||
|
||||
// stream return whether or not we are currently discovering services
|
||||
@Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023
|
||||
Stream<bool> get isDiscoveringServices => _isDiscoveringServices.stream;
|
||||
|
||||
/// Get services
|
||||
/// - returns empty if discoverServices() has not been called
|
||||
/// or if your device does not have any services (rare)
|
||||
List<BluetoothServiceWindows> get servicesList => FlutterBluePlusWindows._knownServices[remoteId] ?? [];
|
||||
|
||||
/// Stream of bluetooth services offered by the remote device
|
||||
/// - this stream is only updated when you call discoverServices()
|
||||
@Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023
|
||||
Stream<List<BluetoothService>> get servicesStream {
|
||||
if (FlutterBluePlusWindows._knownServices[remoteId] != null) {
|
||||
return _services.stream.newStreamWithInitialValue(
|
||||
FlutterBluePlusWindows._knownServices[remoteId]!,
|
||||
);
|
||||
} else {
|
||||
return _services.stream;
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a subscription to be canceled when the device is disconnected.
|
||||
/// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions.
|
||||
/// - this is an optional convenience function
|
||||
/// - prevents accidentally creating duplicate subscriptions on each reconnection.
|
||||
/// - [next] if true, the the stream will be canceled only on the *next* disconnection.
|
||||
/// This is useful if you setup your subscriptions before you connect.
|
||||
/// - [delayed] Note: This option is only meant for `connectionState` subscriptions.
|
||||
/// When `true`, we cancel after a small delay. This ensures the `connectionState`
|
||||
/// listener receives the `disconnected` event.
|
||||
void cancelWhenDisconnected(StreamSubscription subscription, {bool next = false, bool delayed = false}) {
|
||||
if (isConnected == false && next == false) {
|
||||
subscription.cancel(); // cancel immediately if already disconnected.
|
||||
} else if (delayed) {
|
||||
FlutterBluePlusWindows._delayedSubscriptions[remoteId] ??= [];
|
||||
FlutterBluePlusWindows._delayedSubscriptions[remoteId]!.add(subscription);
|
||||
} else {
|
||||
FlutterBluePlusWindows._deviceSubscriptions[remoteId] ??= [];
|
||||
FlutterBluePlusWindows._deviceSubscriptions[remoteId]!.add(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this device is currently connected to your app
|
||||
bool get isConnected {
|
||||
return FlutterBluePlusWindows.connectedDevices.contains(this);
|
||||
}
|
||||
|
||||
/// Returns true if this device is currently disconnected from your app
|
||||
bool get isDisconnected => isConnected == false;
|
||||
|
||||
Future<void> connect({
|
||||
Duration? timeout = const Duration(seconds: 35), // TODO: implementation missing
|
||||
bool autoConnect = false, // TODO: implementation missing
|
||||
int? mtu = 512, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.connect(_address);
|
||||
FlutterBluePlusWindows._deviceSet.add(this);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect({
|
||||
int androidDelay = 2000, // TODO: implementation missing
|
||||
int timeout = 35, // TODO: implementation missing
|
||||
bool queue = true, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.disconnect(_address);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} finally {
|
||||
FlutterBluePlusWindows._deviceSet.remove(this);
|
||||
|
||||
FlutterBluePlusWindows._deviceSubscriptions[remoteId]?.forEach((s) => s.cancel());
|
||||
FlutterBluePlusWindows._deviceSubscriptions.remove(remoteId);
|
||||
// use delayed to update the stream before we cancel it
|
||||
Future.delayed(Duration.zero).then((_) {
|
||||
FlutterBluePlusWindows._delayedSubscriptions[remoteId]?.forEach((s) => s.cancel());
|
||||
FlutterBluePlusWindows._delayedSubscriptions.remove(remoteId);
|
||||
});
|
||||
|
||||
FlutterBluePlusWindows._lastChrs[remoteId]?.clear();
|
||||
FlutterBluePlusWindows._isNotifying[remoteId]?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<BluetoothService>> discoverServices({
|
||||
bool subscribeToServicesChanged = true, // TODO: implementation missing
|
||||
int timeout = 15, // TODO: implementation missing
|
||||
}) async {
|
||||
List<BluetoothServiceWindows> result = List.from(FlutterBluePlusWindows._knownServices[remoteId] ?? []);
|
||||
|
||||
try {
|
||||
_isDiscoveringServices.add(true);
|
||||
|
||||
final response = await WinBle.discoverServices(_address);
|
||||
FlutterBluePlusWindows._characteristicCache[remoteId] ??= <String, List<BluetoothCharacteristic>>{};
|
||||
|
||||
for (final serviceId in response) {
|
||||
final characteristic = await WinBle.discoverCharacteristics(
|
||||
address: _address,
|
||||
serviceId: serviceId,
|
||||
);
|
||||
FlutterBluePlusWindows._characteristicCache[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._characteristicCache[remoteId]?[serviceId] ??= [
|
||||
...characteristic.map(
|
||||
(e) => BluetoothCharacteristicWindows(
|
||||
remoteId: remoteId,
|
||||
serviceUuid: Guid(serviceId),
|
||||
characteristicUuid: Guid(e.uuid),
|
||||
descriptors: [],
|
||||
// TODO: implementation missing
|
||||
propertiesWinBle: e.properties,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
result = [
|
||||
...response.map(
|
||||
(p) => BluetoothServiceWindows(
|
||||
remoteId: remoteId,
|
||||
serviceUuid: Guid(p),
|
||||
// TODO: implementation missing
|
||||
isPrimary: true,
|
||||
// TODO: implementation missing
|
||||
characteristics: FlutterBluePlusWindows._characteristicCache[remoteId]![p]!,
|
||||
// TODO: implementation missing
|
||||
includedServices: [],
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
FlutterBluePlusWindows._knownServices[remoteId] = result;
|
||||
|
||||
_services.add(result);
|
||||
} finally {
|
||||
_isDiscoveringServices.add(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
DisconnectReason? get disconnectReason {
|
||||
// TODO: nothing to do
|
||||
return null;
|
||||
}
|
||||
|
||||
Stream<BluetoothConnectionState> get connectionState async* {
|
||||
await FlutterBluePlusWindows._initialize();
|
||||
|
||||
final map = FlutterBluePlusWindows._connectionStream.latestValue;
|
||||
|
||||
if (map[_address] != null) {
|
||||
yield map[_address]!.isConnected;
|
||||
}
|
||||
|
||||
yield* WinBle.connectionStreamOf(_address).map((e) => e.isConnected);
|
||||
}
|
||||
|
||||
Stream<int> get mtu async* {
|
||||
bool isEmitted = false;
|
||||
int retryCount = 0;
|
||||
while (!isEmitted) {
|
||||
if (retryCount > 3) throw "Device not found!";
|
||||
retryCount++;
|
||||
try {
|
||||
yield await WinBle.getMaxMtuSize(_address);
|
||||
isEmitted = true;
|
||||
} catch (e) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> readRssi({int timeout = 15}) async {
|
||||
return FlutterBluePlusWindows._rssiMap[remoteId] ?? -100;
|
||||
}
|
||||
|
||||
Future<int> requestMtu(
|
||||
int desiredMtu, {
|
||||
double predelay = 0.35,
|
||||
int timeout = 15,
|
||||
}) async {
|
||||
// https://github.com/rohitsangwan01/win_ble/issues/8
|
||||
return await WinBle.getMaxMtuSize(_address);
|
||||
}
|
||||
|
||||
Future<void> requestConnectionPriority({
|
||||
required ConnectionPriority connectionPriorityRequest,
|
||||
}) async {
|
||||
// TODO: nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
/// Set the preferred connection (Android Only)
|
||||
/// - [txPhy] bitwise OR of all allowed phys for Tx, e.g. (Phy.le2m.mask | Phy.leCoded.mask)
|
||||
/// - [txPhy] bitwise OR of all allowed phys for Rx, e.g. (Phy.le2m.mask | Phy.leCoded.mask)
|
||||
/// - [option] preferred coding to use when transmitting on Phy.leCoded
|
||||
/// Please note that this is just a recommendation given to the system.
|
||||
Future<void> setPreferredPhy({
|
||||
required int txPhy,
|
||||
required int rxPhy,
|
||||
required PhyCoding option,
|
||||
}) async {
|
||||
// TODO: implementation missing
|
||||
}
|
||||
|
||||
Future<void> createBond({
|
||||
Uint8List? pin,
|
||||
int timeout = 90, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.pair(_address);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeBond({
|
||||
int timeout = 30, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.unPair(_address);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearGattCache() async {
|
||||
// TODO: implementation missing
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is BluetoothDeviceWindows && runtimeType == other.runtimeType && remoteId == other.remoteId);
|
||||
|
||||
@override
|
||||
int get hashCode => remoteId.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BluetoothDevice{'
|
||||
'remoteId: $remoteId, '
|
||||
'platformName: $platformName, '
|
||||
'services: ${FlutterBluePlusWindows._knownServices[remoteId]}'
|
||||
'}';
|
||||
}
|
||||
|
||||
@Deprecated('Use createBond() instead')
|
||||
Future<void> pair() async => await createBond();
|
||||
|
||||
@Deprecated('Use remoteId instead')
|
||||
DeviceIdentifier get id => remoteId;
|
||||
|
||||
@Deprecated('Use localName instead')
|
||||
String get name => localName;
|
||||
|
||||
@Deprecated('Use connectionState instead')
|
||||
Stream<BluetoothConnectionState> get state => connectionState;
|
||||
|
||||
@Deprecated('Use servicesStream instead')
|
||||
Stream<List<BluetoothService>> get services => servicesStream;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
class BluetoothServiceWindows extends BluetoothService {
|
||||
final DeviceIdentifier remoteId;
|
||||
final Guid serviceUuid;
|
||||
final bool isPrimary;
|
||||
final List<BluetoothCharacteristic> characteristics;
|
||||
final List<BluetoothService> includedServices;
|
||||
|
||||
BluetoothServiceWindows({
|
||||
required this.remoteId,
|
||||
required this.serviceUuid,
|
||||
required this.isPrimary,
|
||||
required this.characteristics,
|
||||
required this.includedServices,
|
||||
}) : super.fromProto(
|
||||
BmBluetoothService(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristics: [for (final c in characteristics) c.toProto()],
|
||||
primaryServiceUuid: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
class FlutterBluePlusWindows {
|
||||
static bool _initialized = false;
|
||||
|
||||
static BluetoothAdapterState _state = BluetoothAdapterState.unknown;
|
||||
|
||||
// stream used for the isScanning public api
|
||||
static final _isScanning = _StreamController(initialValue: false);
|
||||
|
||||
// we always keep track of these device variables
|
||||
static final _platformNames = <DeviceIdentifier, String>{};
|
||||
static final _advNames = <DeviceIdentifier, String>{};
|
||||
static final _rssiMap = <DeviceIdentifier, int?>{};
|
||||
static final _knownServices = <DeviceIdentifier, List<BluetoothServiceWindows>>{};
|
||||
static final Map<DeviceIdentifier, Map<String, List<int>>> _lastChrs = {};
|
||||
static final Map<DeviceIdentifier, Map<String, bool>> _isNotifying = {};
|
||||
static final Map<DeviceIdentifier, Map<String, List<BluetoothCharacteristic>>> _characteristicCache = {};
|
||||
static final Map<DeviceIdentifier, List<StreamSubscription>> _deviceSubscriptions = {};
|
||||
static final Map<DeviceIdentifier, List<StreamSubscription>> _delayedSubscriptions = {};
|
||||
static final List<StreamSubscription> _scanSubscriptions = [];
|
||||
|
||||
// stream used for the scanResults public api
|
||||
static final _scanResultsList = _StreamController(initialValue: <ScanResult>[]);
|
||||
|
||||
// the subscription to the scan results stream
|
||||
static StreamSubscription<BleDevice?>? _scanSubscription;
|
||||
|
||||
// timeout for scanning that can be cancelled by stopScan
|
||||
static Timer? _scanTimeout;
|
||||
|
||||
static List<BluetoothDeviceWindows> get _devices => [..._deviceSet];
|
||||
|
||||
static final _deviceSet = <BluetoothDeviceWindows>{};
|
||||
static final _removedDeviceTracer = <BluetoothDeviceWindows, StreamSubscription>{};
|
||||
|
||||
// static final _unhandledDeviceSet = <BluetoothDeviceWindows>{};
|
||||
|
||||
/// Flutter blue plus windows
|
||||
static final _charReadWriteStreamController = StreamController<(String, List<int>)>();
|
||||
static final _charReadStreamController = StreamController<(String, List<int>)>();
|
||||
|
||||
static final _charReadWriteStream = _charReadWriteStreamController.stream.asBroadcastStream();
|
||||
static final _charReadStream = _charReadStreamController.stream.asBroadcastStream();
|
||||
|
||||
/// Flutter blue plus windows
|
||||
static final _connectionStream = _StreamController(initialValue: <String, bool>{});
|
||||
|
||||
static Future<void> _initialize() async {
|
||||
if (_initialized) return;
|
||||
await WinBle.initialize(
|
||||
serverPath: await WinServer.path(),
|
||||
enableLog: false,
|
||||
);
|
||||
|
||||
WinBle.connectionStream.listen(
|
||||
(event) {
|
||||
log('$event - event');
|
||||
if (event['device'] == null) return;
|
||||
if (event['connected'] == null) return;
|
||||
|
||||
final map = _connectionStream.latestValue;
|
||||
map[event['device']] = event['connected'];
|
||||
|
||||
log('$map - map');
|
||||
_connectionStream.add(map);
|
||||
|
||||
if (!event['connected']) {
|
||||
final removingDevices = [
|
||||
..._deviceSet.where(
|
||||
(device) => device._address == event['device'],
|
||||
),
|
||||
];
|
||||
for (final device in removingDevices) {
|
||||
_deviceSet.remove(device);
|
||||
if (!_removedDeviceTracer.keys.contains(device)) {
|
||||
_removedDeviceTracer[device] = Stream.periodic(const Duration(seconds: 10), (_) => device).listen(
|
||||
(event) {
|
||||
if(event.isConnected) {
|
||||
_removedDeviceTracer[device]?.cancel();
|
||||
_removedDeviceTracer.remove(device);
|
||||
return;
|
||||
}
|
||||
event.connect();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_deviceSubscriptions[device.remoteId]?.forEach((s) => s.cancel());
|
||||
_deviceSubscriptions.remove(device.remoteId);
|
||||
// use delayed to update the stream before we cancel it
|
||||
Future.delayed(Duration.zero).then((_) {
|
||||
_delayedSubscriptions[device.remoteId]?.forEach((s) => s.cancel());
|
||||
_delayedSubscriptions.remove(device.remoteId);
|
||||
});
|
||||
|
||||
_lastChrs[device.remoteId]?.clear();
|
||||
_isNotifying[device.remoteId]?.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
static Future<bool> get isSupported async {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<String> get adapterName async {
|
||||
return 'Windows';
|
||||
}
|
||||
|
||||
static Stream<bool> get isScanning => _isScanning.stream;
|
||||
|
||||
static bool get isScanningNow => _isScanning.latestValue;
|
||||
|
||||
static Future<void> turnOn({int timeout = 10}) async {
|
||||
await _initialize();
|
||||
await WinBle.updateBluetoothState(true);
|
||||
}
|
||||
|
||||
// TODO: compare with original lib
|
||||
static Stream<List<ScanResult>> get scanResults => _scanResultsList.stream;
|
||||
|
||||
static Stream<BluetoothAdapterState> get adapterState async* {
|
||||
await _initialize();
|
||||
yield _state;
|
||||
yield* WinBle.bleState.asBroadcastStream().map(
|
||||
(s) {
|
||||
_state = s.toAdapterState();
|
||||
return _state;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Start a scan, and return a stream of results
|
||||
/// Note: scan filters use an "or" behavior. i.e. if you set `withServices` & `withNames` we
|
||||
/// return all the advertisments that match any of the specified services *or* any of the specified names.
|
||||
/// - [withServices] filter by advertised services
|
||||
/// - [withRemoteIds] filter for known remoteIds (iOS: 128-bit guid, android: 48-bit mac address)
|
||||
/// - [withNames] filter by advertised names (exact match)
|
||||
/// - [withKeywords] filter by advertised names (matches any substring)
|
||||
/// - [withMsd] filter by manfacture specific data
|
||||
/// - [withServiceData] filter by service data
|
||||
/// - [timeout] calls stopScan after a specified duration
|
||||
/// - [removeIfGone] if true, remove devices after they've stopped advertising for X duration
|
||||
/// - [continuousUpdates] If `true`, we continually update 'lastSeen' & 'rssi' by processing
|
||||
/// duplicate advertisements. This takes more power. You typically should not use this option.
|
||||
/// - [continuousDivisor] Useful to help performance. If divisor is 3, then two-thirds of advertisements are
|
||||
/// ignored, and one-third are processed. This reduces main-thread usage caused by the platform channel.
|
||||
/// The scan counting is per-device so you always get the 1st advertisement from each device.
|
||||
/// If divisor is 1, all advertisements are returned. This argument only matters for `continuousUpdates` mode.
|
||||
/// - [oneByOne] if `true`, we will stream every advertistment one by one, possibly including duplicates.
|
||||
/// If `false`, we deduplicate the advertisements, and return a list of devices.
|
||||
/// - [androidLegacy] Android only. If `true`, scan on 1M phy only.
|
||||
/// If `false`, scan on all supported phys. How the radio cycles through all the supported phys is purely
|
||||
/// dependent on the your Bluetooth stack implementation.
|
||||
/// - [androidScanMode] choose the android scan mode to use when scanning
|
||||
/// - [androidUsesFineLocation] request `ACCESS_FINE_LOCATION` permission at runtime
|
||||
static Future<void> startScan({
|
||||
List<Guid> withServices = const [],
|
||||
List<String> withRemoteIds = const [],
|
||||
List<String> withNames = const [],
|
||||
//TODO: implementation missing
|
||||
List<String> withKeywords = const [],
|
||||
//TODO: implementation missing
|
||||
List<MsdFilter> withMsd = const [],
|
||||
List<ServiceDataFilter> withServiceData = const [],
|
||||
Duration? timeout,
|
||||
Duration? removeIfGone,
|
||||
bool continuousUpdates = false,
|
||||
int continuousDivisor = 1,
|
||||
bool oneByOne = false,
|
||||
bool androidLegacy = false,
|
||||
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
|
||||
bool androidUsesFineLocation = false,
|
||||
}) async {
|
||||
await _initialize();
|
||||
|
||||
// stop existing scan
|
||||
if (_isScanning.latestValue == true) {
|
||||
await stopScan();
|
||||
}
|
||||
|
||||
// push to stream
|
||||
_isScanning.add(true);
|
||||
|
||||
// Start timer *after* stream is being listened to, to make sure the
|
||||
// timeout does not fire before _buffer is set
|
||||
if (timeout != null) {
|
||||
_scanTimeout = Timer(timeout, stopScan);
|
||||
}
|
||||
|
||||
/// remove connection by OS.
|
||||
/// The reason why we add this logic is
|
||||
/// to avoid uncontrollable devices and to make consistency.
|
||||
|
||||
/// add WinBle scanning
|
||||
WinBle.startScanning();
|
||||
|
||||
// check every 250ms for gone devices?
|
||||
late Stream<BleDevice?> outputStream;
|
||||
if (removeIfGone != null) {
|
||||
outputStream = _mergeStreams([WinBle.scanStream, Stream.periodic(Duration(milliseconds: 250))]);
|
||||
} else {
|
||||
outputStream = WinBle.scanStream;
|
||||
}
|
||||
|
||||
final output = <ScanResult>[];
|
||||
|
||||
// listen & push to `scanResults` stream
|
||||
_scanSubscription = outputStream.listen(
|
||||
(BleDevice? winBleDevice) {
|
||||
// print(winBleDevice?.serviceUuids);
|
||||
if (winBleDevice == null) {
|
||||
// if null, this is just a periodic update for removing old results
|
||||
output.removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!);
|
||||
|
||||
// push to stream
|
||||
_scanResultsList.add(List.from(output));
|
||||
} else {
|
||||
final remoteId = DeviceIdentifier(winBleDevice.address.toUpperCase());
|
||||
final scanResult = output.where((sr) => sr.device.remoteId == remoteId).firstOrNull;
|
||||
final deviceName = winBleDevice.name.isNotEmpty ? winBleDevice.name : scanResult?.device.platformName ?? '';
|
||||
final serviceUuids = winBleDevice.serviceUuids.isNotEmpty
|
||||
? [...winBleDevice.serviceUuids.map((e) => Guid((e as String).replaceAll(RegExp(r'[{}]'), '')))]
|
||||
: scanResult?.advertisementData.serviceUuids ?? [];
|
||||
|
||||
final manufacturerData = winBleDevice.manufacturerData.isNotEmpty
|
||||
? {
|
||||
if (winBleDevice.manufacturerData.length >= 2)
|
||||
winBleDevice.manufacturerData[0] + (winBleDevice.manufacturerData[1] << 8):
|
||||
winBleDevice.manufacturerData.sublist(2),
|
||||
}
|
||||
: scanResult?.advertisementData.manufacturerData ?? {};
|
||||
|
||||
final rssi = int.tryParse(winBleDevice.rssi) ?? -100;
|
||||
|
||||
FlutterBluePlusWindows._platformNames[remoteId] = deviceName;
|
||||
FlutterBluePlusWindows._advNames[remoteId] = deviceName;
|
||||
FlutterBluePlusWindows._rssiMap[remoteId] = rssi;
|
||||
|
||||
final device = BluetoothDeviceWindows(remoteId: remoteId);
|
||||
|
||||
String hex(int value) => value.toRadixString(16).padLeft(2, '0');
|
||||
String hexToId(Iterable<int> values) => values.map((e) => hex(e)).join();
|
||||
|
||||
final sr = ScanResult(
|
||||
device: device,
|
||||
advertisementData: AdvertisementData(
|
||||
advName: deviceName,
|
||||
txPowerLevel: winBleDevice.adStructures?.where((e) => e.type == 10).singleOrNull?.data.firstOrNull,
|
||||
//TODO: Should verify
|
||||
connectable: !winBleDevice.advType.contains('Non'),
|
||||
manufacturerData: manufacturerData,
|
||||
serviceData: {
|
||||
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
|
||||
if (advStructures.type == 0x16 && advStructures.data.length >= 2)
|
||||
Guid(hexToId(advStructures.data.sublist(0, 2).reversed)): advStructures.data.sublist(2),
|
||||
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
|
||||
if (advStructures.type == 0x20 && advStructures.data.length >= 4)
|
||||
Guid(hexToId(advStructures.data.sublist(0, 4).reversed)): advStructures.data.sublist(4),
|
||||
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
|
||||
if (advStructures.type == 0x21 && advStructures.data.length >= 16)
|
||||
Guid(hexToId(advStructures.data.sublist(0, 16).reversed)): advStructures.data.sublist(16),
|
||||
},
|
||||
serviceUuids: serviceUuids,
|
||||
appearance: null,
|
||||
),
|
||||
rssi: rssi,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
// filter with services
|
||||
final isFilteredWithServices =
|
||||
withServices.isNotEmpty && serviceUuids.where((service) => withServices.contains(service)).isEmpty;
|
||||
|
||||
// filter with remote ids
|
||||
final isFilteredWithRemoteIds = withRemoteIds.isNotEmpty && !withRemoteIds.contains(remoteId);
|
||||
|
||||
// filter with names
|
||||
final isFilteredWithNames = withNames.isNotEmpty && !withNames.contains(deviceName);
|
||||
|
||||
if (isFilteredWithServices || isFilteredWithRemoteIds || isFilteredWithNames) {
|
||||
_scanResultsList.add(List.from(output));
|
||||
return;
|
||||
}
|
||||
|
||||
// add result to output
|
||||
if (oneByOne) {
|
||||
output
|
||||
..clear()
|
||||
..add(sr);
|
||||
} else {
|
||||
output.addOrUpdate(sr);
|
||||
}
|
||||
|
||||
// push to stream
|
||||
_scanResultsList.add(List.from(output));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static List<FBP.BluetoothDevice> get connectedDevices {
|
||||
return _devices;
|
||||
}
|
||||
|
||||
static Future<List<BluetoothDeviceWindows>> get bondedDevices async {
|
||||
return _devices;
|
||||
}
|
||||
|
||||
/// Stops a scan for Bluetooth Low Energy devices
|
||||
static Future<void> stopScan() async {
|
||||
await _initialize();
|
||||
WinBle.stopScanning();
|
||||
_scanSubscription?.cancel();
|
||||
_scanTimeout?.cancel();
|
||||
_isScanning.add(false);
|
||||
|
||||
for (var subscription in _scanSubscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
|
||||
_scanResultsList.latestValue = [];
|
||||
}
|
||||
|
||||
/// Register a subscription to be canceled when scanning is complete.
|
||||
/// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions.
|
||||
/// - this is an optional convenience function
|
||||
/// - prevents accidentally creating duplicate subscriptions before each scan
|
||||
static void cancelWhenScanComplete(StreamSubscription subscription) {
|
||||
_scanSubscriptions.add(subscription);
|
||||
}
|
||||
|
||||
/// Sets the internal FlutterBlue log level
|
||||
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
|
||||
// Nothing to implement
|
||||
return;
|
||||
}
|
||||
|
||||
static Future<void> turnOff({int timeout = 10}) async {
|
||||
await _initialize();
|
||||
await WinBle.updateBluetoothState(false);
|
||||
}
|
||||
|
||||
// TODO: need to test
|
||||
static Future<bool> get isOn async {
|
||||
await _initialize();
|
||||
return await WinBle.bleState.asBroadcastStream().first == BleState.On;
|
||||
}
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
String _hexEncode(List<int> numbers) {
|
||||
return numbers
|
||||
.map((n) => (n & 0xFF).toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
}
|
||||
|
||||
List<int> _hexDecode(String hex) {
|
||||
List<int> numbers = [];
|
||||
for (int i = 0; i < hex.length; i += 2) {
|
||||
String hexPart = hex.substring(i, i + 2);
|
||||
int num = int.parse(hexPart, radix: 16);
|
||||
numbers.add(num);
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
int _compareAsciiLowerCase(String a, String b) {
|
||||
const int upperCaseA = 0x41;
|
||||
const int upperCaseZ = 0x5a;
|
||||
const int asciiCaseBit = 0x20;
|
||||
var defaultResult = 0;
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (i >= b.length) return 1;
|
||||
var aChar = a.codeUnitAt(i);
|
||||
var bChar = b.codeUnitAt(i);
|
||||
if (aChar == bChar) continue;
|
||||
var aLowerCase = aChar;
|
||||
var bLowerCase = bChar;
|
||||
// Upper case if ASCII letters.
|
||||
if (upperCaseA <= bChar && bChar <= upperCaseZ) {
|
||||
bLowerCase += asciiCaseBit;
|
||||
}
|
||||
if (upperCaseA <= aChar && aChar <= upperCaseZ) {
|
||||
aLowerCase += asciiCaseBit;
|
||||
}
|
||||
if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign;
|
||||
if (defaultResult == 0) defaultResult = aChar - bChar;
|
||||
}
|
||||
if (b.length > a.length) return -1;
|
||||
return defaultResult.sign;
|
||||
}
|
||||
|
||||
// This is a reimplementation of BehaviorSubject from RxDart library.
|
||||
// It is essentially a stream but:
|
||||
// 1. we cache the latestValue of the stream
|
||||
// 2. the "latestValue" is re-emitted whenever the stream is listened to
|
||||
class _StreamController<T> {
|
||||
T latestValue;
|
||||
|
||||
final StreamController<T> _controller = StreamController<T>.broadcast();
|
||||
|
||||
_StreamController({required T initialValue})
|
||||
: this.latestValue = initialValue;
|
||||
|
||||
Stream<T> get stream => _controller.stream;
|
||||
|
||||
T get value => latestValue;
|
||||
|
||||
void add(T newValue) {
|
||||
latestValue = newValue;
|
||||
_controller.add(newValue);
|
||||
}
|
||||
|
||||
void listen(Function(T) onData,
|
||||
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
|
||||
onData(latestValue);
|
||||
_controller.stream.listen(onData,
|
||||
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
Future<void> close() {
|
||||
return _controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
// imediately starts listening to a broadcast stream and
|
||||
// buffering it in a new single-subscription stream
|
||||
class _BufferStream<T> {
|
||||
final Stream<T> _inputStream;
|
||||
late final StreamSubscription? _subscription;
|
||||
late final StreamController<T> _controller;
|
||||
late bool hasReceivedValue = false;
|
||||
|
||||
_BufferStream.listen(this._inputStream) {
|
||||
_controller = StreamController<T>(
|
||||
onCancel: () {
|
||||
_subscription?.cancel();
|
||||
},
|
||||
onPause: () {
|
||||
_subscription?.pause();
|
||||
},
|
||||
onResume: () {
|
||||
_subscription?.resume();
|
||||
},
|
||||
onListen: () {}, // inputStream is already listened to
|
||||
);
|
||||
|
||||
// immediately start listening to the inputStream
|
||||
_subscription = _inputStream.listen(
|
||||
(data) {
|
||||
hasReceivedValue = true;
|
||||
_controller.add(data);
|
||||
},
|
||||
onError: (e) {
|
||||
_controller.addError(e);
|
||||
},
|
||||
onDone: () {
|
||||
_controller.close();
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void close() {
|
||||
_subscription?.cancel();
|
||||
_controller.close();
|
||||
}
|
||||
|
||||
Stream<T> get stream async* {
|
||||
yield* _controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
// helper for 'doOnDone' method for streams.
|
||||
class _OnDoneTransformer<T> extends StreamTransformerBase<T, T> {
|
||||
final Function onDone;
|
||||
|
||||
_OnDoneTransformer({required this.onDone});
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
if (stream.isBroadcast) {
|
||||
return _bindBroadcast(stream);
|
||||
}
|
||||
return _bindSingleSubscription(stream);
|
||||
}
|
||||
|
||||
Stream<T> _bindSingleSubscription(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>(
|
||||
onListen: () {
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: controller?.addError,
|
||||
onDone: () {
|
||||
onDone();
|
||||
controller?.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
onPause: ([Future<dynamic>? resumeSignal]) {
|
||||
subscription?.pause(resumeSignal);
|
||||
},
|
||||
onResume: () {
|
||||
subscription?.resume();
|
||||
},
|
||||
onCancel: () {
|
||||
return subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<T> _bindBroadcast(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>.broadcast(
|
||||
onListen: () {
|
||||
subscription = stream
|
||||
.listen(controller?.add, onError: controller?.addError, onDone: () {
|
||||
onDone();
|
||||
controller?.close();
|
||||
});
|
||||
},
|
||||
onCancel: () {
|
||||
subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
// helper for 'doOnCancel' method for streams.
|
||||
class _OnCancelTransformer<T> extends StreamTransformerBase<T, T> {
|
||||
final Function onCancel;
|
||||
|
||||
_OnCancelTransformer({required this.onCancel});
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
if (stream.isBroadcast) {
|
||||
return _bindBroadcast(stream);
|
||||
}
|
||||
|
||||
return _bindSingleSubscription(stream);
|
||||
}
|
||||
|
||||
Stream<T> _bindSingleSubscription(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>(
|
||||
onListen: () {
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: (Object error) {
|
||||
controller?.addError(error);
|
||||
controller?.close();
|
||||
},
|
||||
onDone: controller?.close,
|
||||
);
|
||||
},
|
||||
onPause: ([Future<dynamic>? resumeSignal]) {
|
||||
subscription?.pause(resumeSignal);
|
||||
},
|
||||
onResume: () {
|
||||
subscription?.resume();
|
||||
},
|
||||
onCancel: () {
|
||||
onCancel();
|
||||
return subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<T> _bindBroadcast(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>.broadcast(
|
||||
onListen: () {
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: (Object error) {
|
||||
controller?.addError(error);
|
||||
controller?.close();
|
||||
},
|
||||
onDone: controller?.close,
|
||||
);
|
||||
},
|
||||
onCancel: () {
|
||||
onCancel();
|
||||
subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for 'newStreamWithInitialValue' method for streams.
|
||||
class _NewStreamWithInitialValueTransformer<T>
|
||||
extends StreamTransformerBase<T, T> {
|
||||
final T initialValue;
|
||||
|
||||
_NewStreamWithInitialValueTransformer(this.initialValue);
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
return _bindSingleSubscription(stream);
|
||||
}
|
||||
|
||||
Stream<T> _bindSingleSubscription(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>(
|
||||
onListen: () {
|
||||
// Emit the initial value
|
||||
controller?.add(initialValue);
|
||||
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: (Object error) {
|
||||
controller?.addError(error);
|
||||
controller?.close();
|
||||
},
|
||||
onDone: controller?.close,
|
||||
);
|
||||
},
|
||||
onPause: ([Future<dynamic>? resumeSignal]) {
|
||||
subscription?.pause(resumeSignal);
|
||||
},
|
||||
onResume: () {
|
||||
subscription?.resume();
|
||||
},
|
||||
onCancel: () {
|
||||
return subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
extension _StreamDoOnDone<T> on Stream<T> {
|
||||
// ignore: unused_element
|
||||
Stream<T> doOnDone(void Function() onDone) {
|
||||
return transform(_OnDoneTransformer(onDone: onDone));
|
||||
}
|
||||
}
|
||||
|
||||
extension _StreamDoOnCancel<T> on Stream<T> {
|
||||
// ignore: unused_element
|
||||
Stream<T> doOnCancel(void Function() onCancel) {
|
||||
return transform(_OnCancelTransformer(onCancel: onCancel));
|
||||
}
|
||||
}
|
||||
|
||||
extension _StreamNewStreamWithInitialValue<T> on Stream<T> {
|
||||
Stream<T> newStreamWithInitialValue(T initialValue) {
|
||||
return transform(_NewStreamWithInitialValueTransformer(initialValue));
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Stream<T> _mergeStreams<T>(List<Stream<T>> streams) {
|
||||
StreamController<T> controller = StreamController<T>();
|
||||
List<StreamSubscription<T>> subscriptions = [];
|
||||
|
||||
void handleData(T data) {
|
||||
if (!controller.isClosed) {
|
||||
controller.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
void handleError(Object error, StackTrace stackTrace) {
|
||||
if (!controller.isClosed) {
|
||||
controller.addError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void handleDone() {
|
||||
if (subscriptions.every((s) => s.isPaused)) {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
void subscribeToStream(Stream<T> stream) {
|
||||
final s =
|
||||
stream.listen(handleData, onError: handleError, onDone: handleDone);
|
||||
subscriptions.add(s);
|
||||
}
|
||||
|
||||
streams.forEach(subscribeToStream);
|
||||
|
||||
controller.onCancel = () async {
|
||||
await Future.wait(subscriptions.map((s) => s.cancel()));
|
||||
};
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
// dart is single threaded, but still has task switching.
|
||||
// this mutex lets a single task through at a time.
|
||||
class _Mutex {
|
||||
final StreamController _controller = StreamController.broadcast();
|
||||
int current = 0;
|
||||
int issued = 0;
|
||||
|
||||
Future<void> take() async {
|
||||
int mine = issued;
|
||||
issued++;
|
||||
// tasks are executed in the same order they call take()
|
||||
while (mine != current) {
|
||||
await _controller.stream.first; // wait
|
||||
}
|
||||
}
|
||||
|
||||
void give() {
|
||||
current++;
|
||||
_controller.add(null); // release waiting tasks
|
||||
}
|
||||
}
|
||||
|
||||
// Create mutexes in a parrallel-safe way,
|
||||
class _MutexFactory {
|
||||
static final _Mutex _global = _Mutex();
|
||||
static final Map<String, _Mutex> _all = {};
|
||||
|
||||
static Future<_Mutex> getMutexForKey(String key) async {
|
||||
_Mutex? value;
|
||||
await _global.take();
|
||||
{
|
||||
_all[key] ??= _Mutex();
|
||||
value = _all[key];
|
||||
}
|
||||
_global.give();
|
||||
return value!;
|
||||
}
|
||||
}
|
||||
|
||||
String _black(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;30m$s\x1B[0m';
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
String _green(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;32m$s\x1B[0m';
|
||||
}
|
||||
|
||||
String _magenta(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;35m$s\x1B[0m';
|
||||
}
|
||||
|
||||
String _brown(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;33m$s\x1B[0m';
|
||||
}
|
||||
|
||||
extension Boolean2ConnectionState on bool {
|
||||
BluetoothConnectionState get isConnected {
|
||||
if (this) return BluetoothConnectionState.connected;
|
||||
return BluetoothConnectionState.disconnected;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP;
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
part 'bluetooth_characteristic_windows.dart';
|
||||
part 'bluetooth_device_windows.dart';
|
||||
part 'bluetooth_service_windows.dart';
|
||||
part 'flutter_blue_plus_windows.dart';
|
||||
part 'util.dart';
|
||||
@@ -1,141 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP;
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
class FlutterBluePlus {
|
||||
static Future<void> startScan({
|
||||
List<Guid> withServices = const [],
|
||||
List<String> withRemoteIds = const [],
|
||||
List<String> withNames = const [],
|
||||
List<String> withKeywords = const [],
|
||||
List<MsdFilter> withMsd = const [],
|
||||
List<ServiceDataFilter> withServiceData = const [],
|
||||
Duration? timeout,
|
||||
Duration? removeIfGone,
|
||||
bool continuousUpdates = false,
|
||||
int continuousDivisor = 1,
|
||||
bool oneByOne = false,
|
||||
bool androidLegacy = false,
|
||||
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
|
||||
bool androidUsesFineLocation = false,
|
||||
List<Guid> webOptionalServices = const [],
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isWindows) {
|
||||
return await FlutterBluePlusWindows.startScan(
|
||||
withServices: withServices,
|
||||
withRemoteIds: withRemoteIds,
|
||||
withNames: withNames,
|
||||
withKeywords: withKeywords,
|
||||
withMsd: withMsd,
|
||||
withServiceData: withServiceData,
|
||||
timeout: timeout,
|
||||
removeIfGone: removeIfGone,
|
||||
continuousUpdates: continuousUpdates,
|
||||
continuousDivisor: continuousDivisor,
|
||||
oneByOne: oneByOne,
|
||||
androidLegacy: androidLegacy,
|
||||
androidScanMode: androidScanMode,
|
||||
androidUsesFineLocation: androidUsesFineLocation,
|
||||
);
|
||||
}
|
||||
|
||||
return await FBP.FlutterBluePlus.startScan(
|
||||
withServices: withServices,
|
||||
withRemoteIds: withRemoteIds,
|
||||
withNames: withNames,
|
||||
withKeywords: withKeywords,
|
||||
withMsd: withMsd,
|
||||
withServiceData: withServiceData,
|
||||
timeout: timeout,
|
||||
removeIfGone: removeIfGone,
|
||||
continuousUpdates: continuousUpdates,
|
||||
continuousDivisor: continuousDivisor,
|
||||
oneByOne: oneByOne,
|
||||
androidLegacy: androidLegacy,
|
||||
androidScanMode: androidScanMode,
|
||||
androidUsesFineLocation: androidUsesFineLocation,
|
||||
webOptionalServices: webOptionalServices,
|
||||
);
|
||||
}
|
||||
|
||||
static Stream<BluetoothAdapterState> get adapterState {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.adapterState;
|
||||
return FBP.FlutterBluePlus.adapterState;
|
||||
}
|
||||
|
||||
static Stream<List<ScanResult>> get scanResults {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.scanResults;
|
||||
return FBP.FlutterBluePlus.scanResults;
|
||||
}
|
||||
|
||||
static bool get isScanningNow {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.isScanningNow;
|
||||
return FBP.FlutterBluePlus.isScanningNow;
|
||||
}
|
||||
|
||||
static Stream<bool> get isScanning {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.isScanning;
|
||||
return FBP.FlutterBluePlus.isScanning;
|
||||
}
|
||||
|
||||
static Future<void> stopScan() async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.stopScan();
|
||||
return await FBP.FlutterBluePlus.stopScan();
|
||||
}
|
||||
|
||||
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.setLogLevel(level, color: color);
|
||||
return FBP.FlutterBluePlus.setLogLevel(level, color: color);
|
||||
}
|
||||
|
||||
/// TODO: need to verify
|
||||
static LogLevel get logLevel => FBP.FlutterBluePlus.logLevel;
|
||||
|
||||
static Future<void> setOptions({bool restoreState = false, bool showPowerAlert = true}) async {
|
||||
if (!kIsWeb && Platform.isWindows) return;
|
||||
FBP.FlutterBluePlus.setOptions(restoreState: restoreState, showPowerAlert: showPowerAlert);
|
||||
}
|
||||
|
||||
static Future<bool> get isSupported async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.isSupported;
|
||||
return await FBP.FlutterBluePlus.isSupported;
|
||||
}
|
||||
|
||||
static Future<String> get adapterName async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.adapterName;
|
||||
return await FBP.FlutterBluePlus.adapterName;
|
||||
}
|
||||
|
||||
static Future<void> turnOn({int timeout = 60}) async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.turnOn(timeout: timeout);
|
||||
return await FBP.FlutterBluePlus.turnOn(timeout: timeout);
|
||||
}
|
||||
|
||||
static List<FBP.BluetoothDevice> get connectedDevices {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
|
||||
return FBP.FlutterBluePlus.connectedDevices;
|
||||
}
|
||||
|
||||
static Future<List<FBP.BluetoothDevice>> systemDevices(List<Guid> withServices) async {
|
||||
//TODO: connected devices => system devices
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
|
||||
return await FBP.FlutterBluePlus.systemDevices(withServices);
|
||||
}
|
||||
|
||||
static Future<PhySupport> getPhySupport() {
|
||||
return FBP.FlutterBluePlus.getPhySupport();
|
||||
}
|
||||
|
||||
static Future<List<FBP.BluetoothDevice>> get bondedDevices async {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
|
||||
return await FBP.FlutterBluePlus.bondedDevices;
|
||||
}
|
||||
|
||||
static void cancelWhenScanComplete(StreamSubscription subscription) {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.cancelWhenScanComplete(subscription);
|
||||
return FBP.FlutterBluePlus.cancelWhenScanComplete(subscription);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export 'flutter_blue_plus_wrapper.dart';
|
||||
@@ -1,13 +0,0 @@
|
||||
name: flutter_blue_plus_windows
|
||||
description: Flutter blue plus for Windows
|
||||
version: 1.26.1
|
||||
repository: https://github.com/chan150/flutter_blue_plus_windows
|
||||
#publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter_blue_plus: ">=1.32.4"
|
||||
win_ble: ">=1.1.1"
|
||||
stream_with_value: ">=0.5.0"
|
||||
12
keypress_simulator/.clang-format
Normal file
12
keypress_simulator/.clang-format
Normal file
@@ -0,0 +1,12 @@
|
||||
# Defines the Chromium style for automatic reformatting.
|
||||
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||
BasedOnStyle: Chromium
|
||||
# This defaults to 'Auto'. Explicitly set it for a while, so that
|
||||
# 'vector<vector<int> >' in existing files gets formatted to
|
||||
# 'vector<vector<int>>'. ('Auto' means that clang-format will only use
|
||||
# 'int>>' if the file already contains at least one such instance.)
|
||||
Standard: Cpp11
|
||||
SortIncludes: true
|
||||
---
|
||||
Language: ObjC
|
||||
ColumnLimit: 100
|
||||
1
keypress_simulator/.github/FUNDING.yml
vendored
Normal file
1
keypress_simulator/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: lijy91
|
||||
50
keypress_simulator/.github/workflows/build.yml
vendored
Normal file
50
keypress_simulator/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.16.8"
|
||||
channel: "stable"
|
||||
- uses: bluefireteam/melos-action@v2
|
||||
- working-directory: ./packages/keypress_simulator/example
|
||||
run: |
|
||||
melos bs
|
||||
flutter build macos --release
|
||||
|
||||
build-web:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.16.8"
|
||||
channel: "stable"
|
||||
- uses: bluefireteam/melos-action@v2
|
||||
- working-directory: ./packages/keypress_simulator/example
|
||||
run: |
|
||||
melos bs
|
||||
flutter build web --release
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.16.8"
|
||||
channel: "stable"
|
||||
- uses: bluefireteam/melos-action@v2
|
||||
- working-directory: ./packages/keypress_simulator/example
|
||||
run: |
|
||||
melos bs
|
||||
flutter build windows --release
|
||||
31
keypress_simulator/.github/workflows/lint.yml
vendored
Normal file
31
keypress_simulator/.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.16.8"
|
||||
channel: "stable"
|
||||
- uses: bluefireteam/melos-action@v2
|
||||
- run: melos run analyze
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.16.8"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
- uses: bluefireteam/melos-action@v2
|
||||
- run: melos run format-check
|
||||
20
keypress_simulator/.github/workflows/test.yml
vendored
Normal file
20
keypress_simulator/.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.16.8"
|
||||
channel: "stable"
|
||||
cache: true
|
||||
- uses: bluefireteam/melos-action@v2
|
||||
- run: melos run test --no-select
|
||||
6
keypress_simulator/.gitignore
vendored
Normal file
6
keypress_simulator/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.dart_tool/
|
||||
.idea/
|
||||
|
||||
*.iml
|
||||
pubspec_overrides.yaml
|
||||
pubspec.lock
|
||||
21
keypress_simulator/LICENSE
Normal file
21
keypress_simulator/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
93
keypress_simulator/README-ZH.md
Normal file
93
keypress_simulator/README-ZH.md
Normal file
@@ -0,0 +1,93 @@
|
||||
> **🚀 快速发布您的应用**: 试试 [Fastforge](https://fastforge.dev) - 构建、打包和分发您的 Flutter 应用最简单的方式。
|
||||
|
||||
# keypress_simulator
|
||||
|
||||
[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url]
|
||||
|
||||
[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg
|
||||
[pub-url]: https://pub.dev/packages/keypress_simulator
|
||||
[discord-image]: https://img.shields.io/discord/884679008049037342.svg
|
||||
[discord-url]: https://discord.gg/zPa6EZ2jqb
|
||||
|
||||
这个插件允许 Flutter 桌面应用模拟按键操作。
|
||||
|
||||
---
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
---
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [平台支持](#%E5%B9%B3%E5%8F%B0%E6%94%AF%E6%8C%81)
|
||||
- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
|
||||
- [安装](#%E5%AE%89%E8%A3%85)
|
||||
- [用法](#%E7%94%A8%E6%B3%95)
|
||||
- [谁在用使用它?](#%E8%B0%81%E5%9C%A8%E7%94%A8%E4%BD%BF%E7%94%A8%E5%AE%83)
|
||||
- [许可证](#%E8%AE%B8%E5%8F%AF%E8%AF%81)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## 平台支持
|
||||
|
||||
| Linux | macOS | Windows |
|
||||
| :---: | :---: | :-----: |
|
||||
| ➖ | ✔️ | ✔️ |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
将此添加到你的软件包的 pubspec.yaml 文件:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
keypress_simulator: ^0.2.0
|
||||
```
|
||||
|
||||
### 用法
|
||||
|
||||
```dart
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
|
||||
// 1. Simulate pressing ⌘ + C
|
||||
|
||||
// 1.1 Simulate key down
|
||||
await keyPressSimulator.simulateKeyDown(
|
||||
PhysicalKeyboardKey.keyC,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
|
||||
// 1.2 Simulate key up
|
||||
await keyPressSimulator.simulateKeyUp(
|
||||
PhysicalKeyboardKey.keyC,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
|
||||
// 2. Simulate long pressing ⌘ + space
|
||||
|
||||
// 2.1. Simulate key down
|
||||
await keyPressSimulator.simulateKeyDown(
|
||||
PhysicalKeyboardKey.space,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
|
||||
// 2.2. Simulate key up
|
||||
await keyPressSimulator.simulateKeyUp(
|
||||
PhysicalKeyboardKey.space,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
```
|
||||
|
||||
> 请看这个插件的示例应用,以了解完整的例子。
|
||||
|
||||
## 谁在用使用它?
|
||||
|
||||
- [Biyi (比译)](https://biyidev.com/) - 一个便捷的翻译和词典应用程序。
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](./LICENSE)
|
||||
93
keypress_simulator/README.md
Normal file
93
keypress_simulator/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
> **🚀 Ship Your App Faster**: Try [Fastforge](https://fastforge.dev) - The simplest way to build, package and distribute your Flutter apps.
|
||||
|
||||
# keypress_simulator
|
||||
|
||||
[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url]
|
||||
|
||||
[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg
|
||||
[pub-url]: https://pub.dev/packages/keypress_simulator
|
||||
[discord-image]: https://img.shields.io/discord/884679008049037342.svg
|
||||
[discord-url]: https://discord.gg/zPa6EZ2jqb
|
||||
|
||||
This plugin allows Flutter desktop apps to simulate key presses.
|
||||
|
||||
---
|
||||
|
||||
English | [简体中文](./README-ZH.md)
|
||||
|
||||
---
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Platform Support](#platform-support)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Who's using it?](#whos-using-it)
|
||||
- [License](#license)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Linux | macOS | Windows |
|
||||
| :---: | :---: | :-----: |
|
||||
| ➖ | ✔️ | ✔️ |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
Add this to your package's pubspec.yaml file:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
keypress_simulator: ^0.2.0
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```dart
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
|
||||
// 1. Simulate pressing ⌘ + C
|
||||
|
||||
// 1.1 Simulate key down
|
||||
await keyPressSimulator.simulateKeyDown(
|
||||
PhysicalKeyboardKey.keyC,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
|
||||
// 1.2 Simulate key up
|
||||
await keyPressSimulator.simulateKeyUp(
|
||||
PhysicalKeyboardKey.keyC,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
|
||||
// 2. Simulate long pressing ⌘ + space
|
||||
|
||||
// 2.1. Simulate key down
|
||||
await keyPressSimulator.simulateKeyDown(
|
||||
PhysicalKeyboardKey.space,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
|
||||
// 2.2. Simulate key up
|
||||
await keyPressSimulator.simulateKeyUp(
|
||||
PhysicalKeyboardKey.space,
|
||||
[ModifierKey.metaModifier],
|
||||
);
|
||||
```
|
||||
|
||||
> Please see the example app of this plugin for a full example.
|
||||
|
||||
## Who's using it?
|
||||
|
||||
- [Biyi (比译)](https://biyidev.com/) - A convenient translation and dictionary app.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
35
keypress_simulator/melos.yaml
Normal file
35
keypress_simulator/melos.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: keypress_simulator_workspace
|
||||
repository: https://github.com/leanflutter/keypress_simulator
|
||||
|
||||
packages:
|
||||
- examples/**
|
||||
- packages/**
|
||||
|
||||
command:
|
||||
bootstrap:
|
||||
# Uses the pubspec_overrides.yaml instead of having Melos modifying the lock file.
|
||||
usePubspecOverrides: true
|
||||
|
||||
scripts:
|
||||
analyze:
|
||||
exec: flutter analyze --fatal-infos
|
||||
description: Run `flutter analyze` for all packages.
|
||||
|
||||
test:
|
||||
exec: flutter test
|
||||
description: Run `flutter test` for a specific package.
|
||||
packageFilters:
|
||||
dirExists:
|
||||
- test
|
||||
|
||||
format:
|
||||
exec: dart format . --fix
|
||||
description: Run `dart format` for all packages.
|
||||
|
||||
format-check:
|
||||
exec: dart format . --fix --set-exit-if-changed
|
||||
description: Run `dart format` checks for all packages.
|
||||
|
||||
fix:
|
||||
exec: dart fix . --apply
|
||||
description: Run `dart fix` for all packages.
|
||||
29
keypress_simulator/packages/keypress_simulator/.gitignore
vendored
Normal file
29
keypress_simulator/packages/keypress_simulator/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
10
keypress_simulator/packages/keypress_simulator/.metadata
Normal file
10
keypress_simulator/packages/keypress_simulator/.metadata
Normal file
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: db747aa1331bd95bc9b3874c842261ca2d302cd5
|
||||
channel: stable
|
||||
|
||||
project_type: plugin
|
||||
@@ -0,0 +1,7 @@
|
||||
## 0.2.0
|
||||
|
||||
* feat: Convert to federated plugin
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* First release.
|
||||
21
keypress_simulator/packages/keypress_simulator/LICENSE
Normal file
21
keypress_simulator/packages/keypress_simulator/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
keypress_simulator/packages/keypress_simulator/README-ZH.md
Symbolic link
1
keypress_simulator/packages/keypress_simulator/README-ZH.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../../README-ZH.md
|
||||
1
keypress_simulator/packages/keypress_simulator/README.md
Symbolic link
1
keypress_simulator/packages/keypress_simulator/README.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../../README.md
|
||||
@@ -0,0 +1 @@
|
||||
include: package:mostly_reasonable_lints/flutter.yaml
|
||||
@@ -0,0 +1 @@
|
||||
export 'src/keypress_simulator.dart';
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:keypress_simulator_platform_interface/keypress_simulator_platform_interface.dart';
|
||||
|
||||
class KeyPressSimulator {
|
||||
KeyPressSimulator._();
|
||||
|
||||
/// The shared instance of [KeyPressSimulator].
|
||||
static final KeyPressSimulator instance = KeyPressSimulator._();
|
||||
|
||||
KeyPressSimulatorPlatform get _platform => KeyPressSimulatorPlatform.instance;
|
||||
|
||||
Future<bool> isAccessAllowed() {
|
||||
return _platform.isAccessAllowed();
|
||||
}
|
||||
|
||||
Future<void> requestAccess({bool onlyOpenPrefPane = false}) {
|
||||
return _platform.requestAccess(onlyOpenPrefPane: onlyOpenPrefPane);
|
||||
}
|
||||
|
||||
Future<void> simulateMouseClick(Offset position) {
|
||||
return _platform.simulateMouseClick(position);
|
||||
}
|
||||
|
||||
/// Simulate key down.
|
||||
Future<void> simulateKeyDown(PhysicalKeyboardKey? key, [List<ModifierKey> modifiers = const []]) {
|
||||
return _platform.simulateKeyPress(key: key, modifiers: modifiers, keyDown: true);
|
||||
}
|
||||
|
||||
/// Simulate key up.
|
||||
Future<void> simulateKeyUp(PhysicalKeyboardKey? key, [List<ModifierKey> modifiers = const []]) {
|
||||
return _platform.simulateKeyPress(key: key, modifiers: modifiers, keyDown: false);
|
||||
}
|
||||
|
||||
@Deprecated('Please use simulateKeyDown & simulateKeyUp methods.')
|
||||
Future<void> simulateCtrlCKeyPress() async {
|
||||
const key = PhysicalKeyboardKey.keyC;
|
||||
final modifiers = Platform.isMacOS ? [ModifierKey.metaModifier] : [ModifierKey.controlModifier];
|
||||
await simulateKeyDown(key, modifiers);
|
||||
await simulateKeyUp(key, modifiers);
|
||||
}
|
||||
|
||||
@Deprecated('Please use simulateKeyDown & simulateKeyUp methods.')
|
||||
Future<void> simulateCtrlVKeyPress() async {
|
||||
const key = PhysicalKeyboardKey.keyV;
|
||||
final modifiers = Platform.isMacOS ? [ModifierKey.metaModifier] : [ModifierKey.controlModifier];
|
||||
await simulateKeyDown(key, modifiers);
|
||||
await simulateKeyUp(key, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
final keyPressSimulator = KeyPressSimulator.instance;
|
||||
36
keypress_simulator/packages/keypress_simulator/pubspec.yaml
Normal file
36
keypress_simulator/packages/keypress_simulator/pubspec.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
name: keypress_simulator
|
||||
description: This plugin allows Flutter desktop apps to simulate key presses.
|
||||
version: 0.2.0
|
||||
homepage: https://github.com/leanflutter/keypress_simulator
|
||||
|
||||
platforms:
|
||||
macos:
|
||||
windows:
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
keypress_simulator_macos:
|
||||
path: ../keypress_simulator_macos
|
||||
keypress_simulator_platform_interface:
|
||||
path: ../keypress_simulator_platform_interface
|
||||
keypress_simulator_windows:
|
||||
path: ../keypress_simulator_windows
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mostly_reasonable_lints: ^0.1.1
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
macos:
|
||||
default_package: keypress_simulator_macos
|
||||
windows:
|
||||
default_package: keypress_simulator_windows
|
||||
|
||||
29
keypress_simulator/packages/keypress_simulator_macos/.gitignore
vendored
Normal file
29
keypress_simulator/packages/keypress_simulator_macos/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4"
|
||||
channel: "stable"
|
||||
|
||||
project_type: plugin
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
- platform: macos
|
||||
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.2.0
|
||||
|
||||
* First release.
|
||||
21
keypress_simulator/packages/keypress_simulator_macos/LICENSE
Normal file
21
keypress_simulator/packages/keypress_simulator_macos/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
# keypress_simulator_macos
|
||||
|
||||
[![pub version][pub-image]][pub-url]
|
||||
|
||||
[pub-image]: https://img.shields.io/pub/v/keypress_simulator_macos.svg
|
||||
[pub-url]: https://pub.dev/packages/keypress_simulator_macos
|
||||
|
||||
The macOS implementation of [keypress_simulator](https://pub.dev/packages/keypress_simulator).
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
@@ -0,0 +1 @@
|
||||
include: package:mostly_reasonable_lints/flutter.yaml
|
||||
@@ -0,0 +1,114 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
public class KeypressSimulatorMacosPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "dev.leanflutter.plugins/keypress_simulator", binaryMessenger: registrar.messenger)
|
||||
let instance = KeypressSimulatorMacosPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "isAccessAllowed":
|
||||
isAccessAllowed(call, result: result)
|
||||
break
|
||||
case "requestAccess":
|
||||
requestAccess(call, result: result)
|
||||
break
|
||||
case "simulateKeyPress":
|
||||
simulateKeyPress(call, result: result)
|
||||
break
|
||||
case "simulateMouseClick":
|
||||
simulateMouseClick(call, result: result)
|
||||
break
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
public func isAccessAllowed(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
result(AXIsProcessTrusted())
|
||||
}
|
||||
|
||||
public func requestAccess(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let args:[String: Any] = call.arguments as! [String: Any]
|
||||
let onlyOpenPrefPane: Bool = args["onlyOpenPrefPane"] as! Bool
|
||||
|
||||
if (!onlyOpenPrefPane) {
|
||||
let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue(): true] as CFDictionary
|
||||
AXIsProcessTrustedWithOptions(options)
|
||||
} else {
|
||||
let prefpaneUrl = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
|
||||
NSWorkspace.shared.open(prefpaneUrl)
|
||||
}
|
||||
result(true)
|
||||
}
|
||||
|
||||
public func simulateKeyPress(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let args:[String: Any] = call.arguments as! [String: Any]
|
||||
|
||||
let keyCode: Int? = args["keyCode"] as? Int
|
||||
let modifiers: Array<String> = args["modifiers"] as! Array<String>
|
||||
let keyDown: Bool = args["keyDown"] as! Bool
|
||||
|
||||
let event = _createKeyPressEvent(keyCode, modifiers, keyDown);
|
||||
event.post(tap: .cghidEventTap);
|
||||
result(true)
|
||||
}
|
||||
|
||||
|
||||
public func simulateMouseClick(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let args:[String: Any] = call.arguments as! [String: Any]
|
||||
|
||||
let x: Double = args["x"] as! Double
|
||||
let y: Double = args["y"] as! Double
|
||||
|
||||
let point = CGPoint(x: x, y: y)
|
||||
|
||||
// Move mouse to the point
|
||||
/*let move = CGEvent(mouseEventSource: nil,
|
||||
mouseType: .mouseMoved,
|
||||
mouseCursorPosition: point,
|
||||
mouseButton: .left)
|
||||
move?.post(tap: .cghidEventTap)*/
|
||||
|
||||
// Mouse down
|
||||
let mouseDown = CGEvent(mouseEventSource: nil,
|
||||
mouseType: .leftMouseDown,
|
||||
mouseCursorPosition: point,
|
||||
mouseButton: .left)
|
||||
mouseDown?.post(tap: .cghidEventTap)
|
||||
|
||||
// Mouse up
|
||||
let mouseUp = CGEvent(mouseEventSource: nil,
|
||||
mouseType: .leftMouseUp,
|
||||
mouseCursorPosition: point,
|
||||
mouseButton: .left)
|
||||
mouseUp?.post(tap: .cghidEventTap)
|
||||
result(true)
|
||||
}
|
||||
|
||||
public func _createKeyPressEvent(_ keyCode: Int?, _ modifiers: Array<String>, _ keyDown: Bool) -> CGEvent {
|
||||
let virtualKey: CGKeyCode = CGKeyCode(UInt32(keyCode ?? 0))
|
||||
var flags: CGEventFlags = CGEventFlags()
|
||||
|
||||
if (modifiers.contains("shiftModifier")) {
|
||||
flags.insert(CGEventFlags.maskShift)
|
||||
}
|
||||
if (modifiers.contains("controlModifier")) {
|
||||
flags.insert(CGEventFlags.maskControl)
|
||||
}
|
||||
if (modifiers.contains("altModifier")) {
|
||||
flags.insert(CGEventFlags.maskAlternate)
|
||||
}
|
||||
if (modifiers.contains("metaModifier")) {
|
||||
flags.insert(CGEventFlags.maskCommand)
|
||||
}
|
||||
if (modifiers.contains("functionModifier")) {
|
||||
flags.insert(CGEventFlags.maskSecondaryFn)
|
||||
}
|
||||
let eventKeyPress = CGEvent(keyboardEventSource: nil, virtualKey: virtualKey, keyDown: keyDown);
|
||||
eventKeyPress!.flags = flags
|
||||
return eventKeyPress!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||
# Run `pod lib lint keypress_simulator_macos.podspec` to validate before publishing.
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'keypress_simulator_macos'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'A new Flutter plugin project.'
|
||||
s.description = <<-DESC
|
||||
A new Flutter plugin project.
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'FlutterMacOS'
|
||||
|
||||
s.platform = :osx, '10.11'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
name: keypress_simulator_macos
|
||||
description: macOS implementation of the keypress_simulator plugin.
|
||||
version: 0.2.0
|
||||
repository: https://github.com/leanflutter/keypress_simulator/tree/main/packages/keypress_simulator_macos
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: '>=3.3.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
keypress_simulator_platform_interface:
|
||||
path: ../keypress_simulator_platform_interface
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mostly_reasonable_lints: ^0.1.1
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
implements: keypress_simulator
|
||||
platforms:
|
||||
macos:
|
||||
pluginClass: KeypressSimulatorMacosPlugin
|
||||
|
||||
29
keypress_simulator/packages/keypress_simulator_platform_interface/.gitignore
vendored
Normal file
29
keypress_simulator/packages/keypress_simulator_platform_interface/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
@@ -0,0 +1,27 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4"
|
||||
channel: "stable"
|
||||
|
||||
project_type: plugin
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.2.0
|
||||
|
||||
* First release.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,16 @@
|
||||
# keypress_simulator_platform_interface
|
||||
|
||||
[![pub version][pub-image]][pub-url]
|
||||
|
||||
[pub-image]: https://img.shields.io/pub/v/keypress_simulator_platform_interface.svg
|
||||
[pub-url]: https://pub.dev/packages/keypress_simulator_platform_interface
|
||||
|
||||
A common platform interface for the [keypress_simulator](https://pub.dev/packages/keypress_simulator) plugin.
|
||||
|
||||
## Usage
|
||||
|
||||
To implement a new platform-specific implementation of keypress_simulator, extend `KeyPressSimulatorPlatform` with an implementation that performs the platform-specific behavior, and when you register your plugin, set the default `KeyPressSimulatorPlatform` by calling `KeyPressSimulatorPlatform.instance = MyPlatformKeyPressSimulator()`.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
@@ -0,0 +1 @@
|
||||
include: package:mostly_reasonable_lints/flutter.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
library keypress_simulator_platform_interface;
|
||||
|
||||
export 'src/keypress_simulator_method_channel.dart';
|
||||
export 'src/keypress_simulator_platform_interface.dart';
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:keypress_simulator_platform_interface/src/keypress_simulator_platform_interface.dart';
|
||||
import 'package:uni_platform/uni_platform.dart';
|
||||
|
||||
/// An implementation of [KeyPressSimulatorPlatform] that uses method channels.
|
||||
class MethodChannelKeyPressSimulator extends KeyPressSimulatorPlatform {
|
||||
/// The method channel used to interact with the native platform.
|
||||
@visibleForTesting
|
||||
final methodChannel = const MethodChannel(
|
||||
'dev.leanflutter.plugins/keypress_simulator',
|
||||
);
|
||||
|
||||
@override
|
||||
Future<bool> isAccessAllowed() async {
|
||||
if (UniPlatform.isMacOS) {
|
||||
return await methodChannel.invokeMethod('isAccessAllowed');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestAccess({
|
||||
bool onlyOpenPrefPane = false,
|
||||
}) async {
|
||||
if (UniPlatform.isMacOS) {
|
||||
final Map<String, dynamic> arguments = {
|
||||
'onlyOpenPrefPane': onlyOpenPrefPane,
|
||||
};
|
||||
await methodChannel.invokeMethod('requestAccess', arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> simulateKeyPress({
|
||||
KeyboardKey? key,
|
||||
List<ModifierKey> modifiers = const [],
|
||||
bool keyDown = true,
|
||||
}) async {
|
||||
PhysicalKeyboardKey? physicalKey = key is PhysicalKeyboardKey ? key : null;
|
||||
if (key is LogicalKeyboardKey) {
|
||||
physicalKey = key.physicalKey;
|
||||
}
|
||||
if (key != null && physicalKey == null) {
|
||||
throw UnsupportedError('Unsupported key: $key.');
|
||||
}
|
||||
final Map<Object?, Object?> arguments = {
|
||||
'keyCode': physicalKey?.keyCode,
|
||||
'modifiers': modifiers.map((e) => e.name).toList(),
|
||||
'keyDown': keyDown,
|
||||
}..removeWhere((key, value) => value == null);
|
||||
await methodChannel.invokeMethod('simulateKeyPress', arguments);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> simulateMouseClick(Offset position) async {
|
||||
final Map<String, double> arguments = {
|
||||
'x': position.dx,
|
||||
'y': position.dy,
|
||||
};
|
||||
await methodChannel.invokeMethod('simulateMouseClick', arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:keypress_simulator_platform_interface/src/keypress_simulator_method_channel.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
|
||||
abstract class KeyPressSimulatorPlatform extends PlatformInterface {
|
||||
/// Constructs a KeyPressSimulatorPlatform.
|
||||
KeyPressSimulatorPlatform() : super(token: _token);
|
||||
|
||||
static final Object _token = Object();
|
||||
|
||||
static KeyPressSimulatorPlatform _instance = MethodChannelKeyPressSimulator();
|
||||
|
||||
/// The default instance of [KeyPressSimulatorPlatform] to use.
|
||||
///
|
||||
/// Defaults to [MethodChannelKeyPressSimulator].
|
||||
static KeyPressSimulatorPlatform get instance => _instance;
|
||||
|
||||
/// Platform-specific implementations should set this with their own
|
||||
/// platform-specific class that extends [KeyPressSimulatorPlatform] when
|
||||
/// they register themselves.
|
||||
static set instance(KeyPressSimulatorPlatform instance) {
|
||||
PlatformInterface.verifyToken(instance, _token);
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
Future<bool> isAccessAllowed() {
|
||||
throw UnimplementedError('isAccessAllowed() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> requestAccess({
|
||||
bool onlyOpenPrefPane = false,
|
||||
}) {
|
||||
throw UnimplementedError('requestAccess() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> simulateKeyPress({
|
||||
KeyboardKey? key,
|
||||
List<ModifierKey> modifiers = const [],
|
||||
bool keyDown = true,
|
||||
}) {
|
||||
throw UnimplementedError('simulateKeyPress() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> simulateMouseClick(Offset position) {
|
||||
throw UnimplementedError('simulateKeyPress() has not been implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
name: keypress_simulator_platform_interface
|
||||
description: A common platform interface for the keypress_simulator plugin.
|
||||
version: 0.2.0
|
||||
homepage: https://github.com/leanflutter/keypress_simulator/blob/main/packages/keypress_simulator_platform_interface
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: '>=3.3.0'
|
||||
|
||||
dependencies:
|
||||
collection: ^1.18.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.1.8
|
||||
uni_platform: ^0.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mostly_reasonable_lints: ^0.1.1
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:keypress_simulator_platform_interface/src/keypress_simulator_method_channel.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
MethodChannelKeyPressSimulator platform = MethodChannelKeyPressSimulator();
|
||||
const MethodChannel channel = MethodChannel(
|
||||
'dev.leanflutter.plugins/keypress_simulator',
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
channel,
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'isAccessAllowed') return true;
|
||||
return '42';
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
|
||||
test('isAccessAllowed', () async {
|
||||
expect(await platform.isAccessAllowed(), true);
|
||||
});
|
||||
}
|
||||
29
keypress_simulator/packages/keypress_simulator_windows/.gitignore
vendored
Normal file
29
keypress_simulator/packages/keypress_simulator_windows/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4"
|
||||
channel: "stable"
|
||||
|
||||
project_type: plugin
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
- platform: windows
|
||||
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.2.0
|
||||
|
||||
* First release.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
# keypress_simulator_windows
|
||||
|
||||
[![pub version][pub-image]][pub-url]
|
||||
|
||||
[pub-image]: https://img.shields.io/pub/v/keypress_simulator_windows.svg
|
||||
[pub-url]: https://pub.dev/packages/keypress_simulator_windows
|
||||
|
||||
The Windows implementation of [keypress_simulator](https://pub.dev/packages/keypress_simulator).
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
@@ -0,0 +1 @@
|
||||
include: package:mostly_reasonable_lints/flutter.yaml
|
||||
@@ -0,0 +1,26 @@
|
||||
name: keypress_simulator_windows
|
||||
description: Windows implementation of the keypress_simulator plugin.
|
||||
version: 0.2.0
|
||||
repository: https://github.com/leanflutter/keypress_simulator/tree/main/packages/keypress_simulator_windows
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: '>=3.3.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
keypress_simulator_platform_interface:
|
||||
path: ../keypress_simulator_platform_interface
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mostly_reasonable_lints: ^0.1.1
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
implements: keypress_simulator
|
||||
platforms:
|
||||
windows:
|
||||
pluginClass: KeypressSimulatorWindowsPluginCApi
|
||||
17
keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore
vendored
Normal file
17
keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
flutter/
|
||||
|
||||
# Visual Studio user-specific files.
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Visual Studio build-related files.
|
||||
x64/
|
||||
x86/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
@@ -0,0 +1,100 @@
|
||||
# The Flutter tooling requires that developers have a version of Visual Studio
|
||||
# installed that includes CMake 3.14 or later. You should not increase this
|
||||
# version, as doing so will cause the plugin to fail to compile for some
|
||||
# customers of the plugin.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
# Project-level configuration.
|
||||
set(PROJECT_NAME "keypress_simulator_windows")
|
||||
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(VERSION 3.14...3.25)
|
||||
|
||||
# This value is used when generating builds using this plugin, so it must
|
||||
# not be changed
|
||||
set(PLUGIN_NAME "keypress_simulator_windows_plugin")
|
||||
|
||||
# Any new source files that you add to the plugin should be added here.
|
||||
list(APPEND PLUGIN_SOURCES
|
||||
"keypress_simulator_windows_plugin.cpp"
|
||||
"keypress_simulator_windows_plugin.h"
|
||||
)
|
||||
|
||||
# Define the plugin library target. Its name must not be changed (see comment
|
||||
# on PLUGIN_NAME above).
|
||||
add_library(${PLUGIN_NAME} SHARED
|
||||
"include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h"
|
||||
"keypress_simulator_windows_plugin_c_api.cpp"
|
||||
${PLUGIN_SOURCES}
|
||||
)
|
||||
|
||||
# Apply a standard set of build settings that are configured in the
|
||||
# application-level CMakeLists.txt. This can be removed for plugins that want
|
||||
# full control over build settings.
|
||||
apply_standard_settings(${PLUGIN_NAME})
|
||||
|
||||
# Symbols are hidden by default to reduce the chance of accidental conflicts
|
||||
# between plugins. This should not be removed; any symbols that should be
|
||||
# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro.
|
||||
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden)
|
||||
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
|
||||
|
||||
# Source include directories and library dependencies. Add any plugin-specific
|
||||
# dependencies here.
|
||||
target_include_directories(${PLUGIN_NAME} INTERFACE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
|
||||
|
||||
# List of absolute paths to libraries that should be bundled with the plugin.
|
||||
# This list could contain prebuilt libraries, or libraries created by an
|
||||
# external build triggered from this build file.
|
||||
set(keypress_simulator_windows_bundled_libraries
|
||||
""
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
# === Tests ===
|
||||
# These unit tests can be run from a terminal after building the example, or
|
||||
# from Visual Studio after opening the generated solution file.
|
||||
|
||||
# Only enable test builds when building the example (which sets this variable)
|
||||
# so that plugin clients aren't building the tests.
|
||||
if (${include_${PROJECT_NAME}_tests})
|
||||
set(TEST_RUNNER "${PROJECT_NAME}_test")
|
||||
enable_testing()
|
||||
|
||||
# Add the Google Test dependency.
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/release-1.11.0.zip
|
||||
)
|
||||
# Prevent overriding the parent project's compiler/linker settings
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
# Disable install commands for gtest so it doesn't end up in the bundle.
|
||||
set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
# The plugin's C API is not very useful for unit testing, so build the sources
|
||||
# directly into the test binary rather than using the DLL.
|
||||
add_executable(${TEST_RUNNER}
|
||||
test/keypress_simulator_windows_plugin_test.cpp
|
||||
${PLUGIN_SOURCES}
|
||||
)
|
||||
apply_standard_settings(${TEST_RUNNER})
|
||||
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
|
||||
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
|
||||
# flutter_wrapper_plugin has link dependencies on the Flutter DLL.
|
||||
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${FLUTTER_LIBRARY}" $<TARGET_FILE_DIR:${TEST_RUNNER}>
|
||||
)
|
||||
|
||||
# Enable automatic test discovery.
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(${TEST_RUNNER})
|
||||
endif()
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
|
||||
#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
|
||||
|
||||
#include <flutter_plugin_registrar.h>
|
||||
|
||||
#ifdef FLUTTER_PLUGIN_IMPL
|
||||
#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
FLUTTER_PLUGIN_EXPORT void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(
|
||||
FlutterDesktopPluginRegistrarRef registrar);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "keypress_simulator_windows_plugin.h"
|
||||
|
||||
// This must be included before many other Windows headers.
|
||||
#include <windows.h>
|
||||
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/plugin_registrar_windows.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
using flutter::EncodableList;
|
||||
using flutter::EncodableMap;
|
||||
using flutter::EncodableValue;
|
||||
|
||||
namespace keypress_simulator_windows {
|
||||
|
||||
// static
|
||||
void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
|
||||
flutter::PluginRegistrarWindows* registrar) {
|
||||
auto channel =
|
||||
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
||||
registrar->messenger(), "dev.leanflutter.plugins/keypress_simulator",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
auto plugin = std::make_unique<KeypressSimulatorWindowsPlugin>();
|
||||
|
||||
channel->SetMethodCallHandler(
|
||||
[plugin_pointer = plugin.get()](const auto& call, auto result) {
|
||||
plugin_pointer->HandleMethodCall(call, std::move(result));
|
||||
});
|
||||
|
||||
registrar->AddPlugin(std::move(plugin));
|
||||
}
|
||||
|
||||
KeypressSimulatorWindowsPlugin::KeypressSimulatorWindowsPlugin() {}
|
||||
|
||||
KeypressSimulatorWindowsPlugin::~KeypressSimulatorWindowsPlugin() {}
|
||||
|
||||
void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
|
||||
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
|
||||
|
||||
UINT keyCode = std::get<int>(args.at(EncodableValue("keyCode")));
|
||||
std::vector<std::string> modifiers;
|
||||
bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown")));
|
||||
|
||||
EncodableList key_modifier_list =
|
||||
std::get<EncodableList>(args.at(EncodableValue("modifiers")));
|
||||
for (flutter::EncodableValue key_modifier_value : key_modifier_list) {
|
||||
std::string key_modifier = std::get<std::string>(key_modifier_value);
|
||||
modifiers.push_back(key_modifier);
|
||||
}
|
||||
|
||||
INPUT input[6];
|
||||
|
||||
for (int32_t i = 0; i < modifiers.size(); i++) {
|
||||
if (modifiers[i].compare("shiftModifier") == 0) {
|
||||
input[i].ki.wVk = VK_SHIFT;
|
||||
} else if (modifiers[i].compare("controlModifier") == 0) {
|
||||
input[i].ki.wVk = VK_CONTROL;
|
||||
} else if (modifiers[i].compare("altModifier") == 0) {
|
||||
input[i].ki.wVk = VK_MENU;
|
||||
} else if (modifiers[i].compare("metaModifier") == 0) {
|
||||
input[i].ki.wVk = VK_LWIN;
|
||||
}
|
||||
|
||||
input[i].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
|
||||
input[i].type = INPUT_KEYBOARD;
|
||||
}
|
||||
|
||||
/*int keyIndex = static_cast<int>(modifiers.size());
|
||||
input[keyIndex].ki.wVk = static_cast<WORD>(keyCode);
|
||||
input[keyIndex].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
|
||||
input[keyIndex].type = INPUT_KEYBOARD;*/
|
||||
|
||||
// Send key sequence to system
|
||||
//SendInput(static_cast<UINT>(std::size(input)), input, sizeof(INPUT));
|
||||
|
||||
BYTE byteValue = static_cast<BYTE>(keyCode);
|
||||
keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);
|
||||
|
||||
result->Success(flutter::EncodableValue(true));
|
||||
}
|
||||
|
||||
void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
|
||||
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
|
||||
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
|
||||
auto it_x = args.find(EncodableValue("x"));
|
||||
if (it_x != args.end() && std::holds_alternative<double>(it_x->second)) {
|
||||
x = std::get<double>(it_x->second);
|
||||
}
|
||||
|
||||
auto it_y = args.find(EncodableValue("y"));
|
||||
if (it_y != args.end() && std::holds_alternative<double>(it_y->second)) {
|
||||
y = std::get<double>(it_y->second);
|
||||
}
|
||||
|
||||
// Move the mouse to the specified coordinates
|
||||
SetCursorPos(static_cast<int>(x), static_cast<int>(y));
|
||||
|
||||
// Prepare input for mouse down and up
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_MOUSE;
|
||||
|
||||
// Mouse left button down
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
// Mouse left button up
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
result->Success(flutter::EncodableValue(true));
|
||||
}
|
||||
|
||||
void KeypressSimulatorWindowsPlugin::HandleMethodCall(
|
||||
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
if (method_call.method_name().compare("simulateKeyPress") == 0) {
|
||||
SimulateKeyPress(method_call, std::move(result));
|
||||
} else if (method_call.method_name().compare("simulateMouseClick") == 0) {
|
||||
SimulateMouseClick(method_call, std::move(result));
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace keypress_simulator_windows
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user