diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..022218ef --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://www.buymeacoffee.com/tsone"] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f06d168d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: CI + +on: [push] + +jobs: + build: + + runs-on: macOS-latest + + steps: + - uses: actions/checkout@master + - name: Start xcodebuild test + run: xcodebuild clean build -project BLEUnlock.xcodeproj -scheme BLEUnlock diff --git a/.gitignore b/.gitignore index 73ef9b50..a62ff656 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,57 @@ -/build -/BLEUnlock.xcodeproj/xcuserdata +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md + +fastlane/report.xml +fastlane/screenshots + +.DS_Store + +/archives diff --git a/BLEUnlock.xcodeproj/project.pbxproj b/BLEUnlock.xcodeproj/project.pbxproj index 9760ab46..eea7daeb 100644 --- a/BLEUnlock.xcodeproj/project.pbxproj +++ b/BLEUnlock.xcodeproj/project.pbxproj @@ -7,18 +7,25 @@ objects = { /* Begin PBXBuildFile section */ + 3D2CF85B255102B300157996 /* MediaRemote.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D2CF85A255102B300157996 /* MediaRemote.framework */; }; 3D2FCF08226C99CB007A06E7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D2FCF07226C99CB007A06E7 /* Images.xcassets */; }; + 3D4EE4112279E55A00AF9E93 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D4EE4102279E55A00AF9E93 /* AppDelegate.m */; }; + 3D4EE4132279E56900AF9E93 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D4EE4122279E56900AF9E93 /* main.m */; }; + 3D4EE4152279E94B00AF9E93 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3D4EE4142279E94B00AF9E93 /* MainMenu.xib */; }; 3D600A37226CC1A40068FB7B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3D600A35226CC1A40068FB7B /* Localizable.strings */; }; - 3D600A4322701A5B0068FB7B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D600A4222701A5B0068FB7B /* AppDelegate.swift */; }; - 3D600A4822701A5C0068FB7B /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3D600A4622701A5C0068FB7B /* MainMenu.xib */; }; 3D600A5A22701E740068FB7B /* Launcher.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3D600A4022701A5A0068FB7B /* Launcher.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3D6070E226DBC68E00EE4E48 /* LEDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6070E126DBC68E00EE4E48 /* LEDeviceInfo.swift */; }; 3D79FD7D226C335100A373C0 /* appleDeviceNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D79FD7C226C335100A373C0 /* appleDeviceNames.swift */; }; - 3DC0BC6D22729050001145EC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3DC0BC6F22729050001145EC /* InfoPlist.strings */; }; + 3D95379522784D550017D8B9 /* AboutBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D95379322784D550017D8B9 /* AboutBox.swift */; }; + 3D953797227853E20017D8B9 /* AboutBox.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3D953799227853E20017D8B9 /* AboutBox.xib */; }; + 3DCAAA7D22ADE9830011A2DA /* checkUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCAAA7C22ADE9830011A2DA /* checkUpdate.swift */; }; 3DD4B654226C1C3200451B7B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD4B653226C1C3200451B7B /* AppDelegate.swift */; }; 3DD4B659226C1C3400451B7B /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3DD4B657226C1C3400451B7B /* MainMenu.xib */; }; 3DD4B664226C1CF000451B7B /* lowlevel.c in Sources */ = {isa = PBXBuildFile; fileRef = 3DD4B663226C1CF000451B7B /* lowlevel.c */; }; 3DD4B666226C1DF200451B7B /* BLE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD4B665226C1DF200451B7B /* BLE.swift */; }; 3DD4B669226C1E3700451B7B /* login.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DD4B668226C1E3700451B7B /* login.framework */; }; + 8E6BAB7636D41F51AE698213 /* GatewayParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74D89DC6A5677427FD9C5C11 /* GatewayParsing.swift */; }; + FCA22F473FD28EC39CD35151 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDCBAD056B2199119AD43EE /* NetworkMonitor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -35,18 +42,32 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3D2CF85A255102B300157996 /* MediaRemote.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemote.framework; path = ../../../../System/Library/PrivateFrameworks/MediaRemote.framework; sourceTree = ""; }; 3D2FCF07226C99CB007A06E7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 3D43D0112765FCCB00D15991 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/AboutBox.strings; sourceTree = ""; }; + 3D43D0122765FCCB00D15991 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 3D43D0132765FCFD00D15991 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/AboutBox.strings; sourceTree = ""; }; + 3D43D0142765FCFD00D15991 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + 3D43D0152765FD1600D15991 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/AboutBox.strings; sourceTree = ""; }; + 3D43D0162765FD1600D15991 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 3D43D0172765FD2800D15991 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/AboutBox.strings; sourceTree = ""; }; + 3D43D0182765FD2800D15991 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + 3D4EE40F2279E55A00AF9E93 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 3D4EE4102279E55A00AF9E93 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 3D4EE4122279E56900AF9E93 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 3D4EE4142279E94B00AF9E93 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 3D600A36226CC1A40068FB7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 3D600A38226CC1AA0068FB7B /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 3D600A4022701A5A0068FB7B /* Launcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Launcher.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 3D600A4222701A5B0068FB7B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 3D600A4722701A5C0068FB7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 3D600A4922701A5C0068FB7B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3D600A4A22701A5C0068FB7B /* Launcher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Launcher.entitlements; sourceTree = ""; }; + 3D6070E126DBC68E00EE4E48 /* LEDeviceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LEDeviceInfo.swift; sourceTree = ""; }; 3D79FD7C226C335100A373C0 /* appleDeviceNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = appleDeviceNames.swift; sourceTree = ""; }; + 3D95379322784D550017D8B9 /* AboutBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutBox.swift; sourceTree = ""; }; + 3D953798227853E20017D8B9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AboutBox.xib; sourceTree = ""; }; + 3D95379B227853EC0017D8B9 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/AboutBox.strings; sourceTree = ""; }; 3DC0BC6922728FF1001145EC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3DC0BC6E22729050001145EC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; - 3DC0BC7022729055001145EC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 3DCAAA7C22ADE9830011A2DA /* checkUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = checkUpdate.swift; sourceTree = ""; }; 3DD4B650226C1C3200451B7B /* BLEUnlock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BLEUnlock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3DD4B653226C1C3200451B7B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3DD4B658226C1C3400451B7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -56,6 +77,12 @@ 3DD4B663226C1CF000451B7B /* lowlevel.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = lowlevel.c; sourceTree = ""; }; 3DD4B665226C1DF200451B7B /* BLE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLE.swift; sourceTree = ""; }; 3DD4B668226C1E3700451B7B /* login.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = login.framework; path = ../../../../System/Library/PrivateFrameworks/login.framework; sourceTree = ""; }; + 5FDCBAD056B2199119AD43EE /* NetworkMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; + 74D89DC6A5677427FD9C5C11 /* GatewayParsing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GatewayParsing.swift; sourceTree = ""; }; + B1EFF91422E010F50010DB0A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/AboutBox.strings"; sourceTree = ""; }; + B1EFF91622E010F50010DB0A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + F749D28C277515C600C78C28 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/AboutBox.strings; sourceTree = ""; }; + F749D28D277515C600C78C28 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,6 +97,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3D2CF85B255102B300157996 /* MediaRemote.framework in Frameworks */, 3DD4B669226C1E3700451B7B /* login.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -80,9 +108,11 @@ 3D600A4122701A5B0068FB7B /* Launcher */ = { isa = PBXGroup; children = ( - 3D600A4222701A5B0068FB7B /* AppDelegate.swift */, - 3D600A4622701A5C0068FB7B /* MainMenu.xib */, + 3D4EE4122279E56900AF9E93 /* main.m */, + 3D4EE40F2279E55A00AF9E93 /* AppDelegate.h */, + 3D4EE4102279E55A00AF9E93 /* AppDelegate.m */, 3D600A4922701A5C0068FB7B /* Info.plist */, + 3D4EE4142279E94B00AF9E93 /* MainMenu.xib */, 3D600A4A22701A5C0068FB7B /* Launcher.entitlements */, ); path = Launcher; @@ -111,11 +141,14 @@ 3DD4B652226C1C3200451B7B /* BLEUnlock */ = { isa = PBXGroup; children = ( + 3D95379322784D550017D8B9 /* AboutBox.swift */, + 3D953799227853E20017D8B9 /* AboutBox.xib */, 3D600A35226CC1A40068FB7B /* Localizable.strings */, 3DD4B665226C1DF200451B7B /* BLE.swift */, + 3D6070E126DBC68E00EE4E48 /* LEDeviceInfo.swift */, 3DD4B653226C1C3200451B7B /* AppDelegate.swift */, 3DC0BC6922728FF1001145EC /* Info.plist */, - 3DC0BC6F22729050001145EC /* InfoPlist.strings */, + 3DCAAA7C22ADE9830011A2DA /* checkUpdate.swift */, 3DD4B662226C1CEF00451B7B /* lowlevel.h */, 3DD4B663226C1CF000451B7B /* lowlevel.c */, 3DD4B657226C1C3400451B7B /* MainMenu.xib */, @@ -123,6 +156,8 @@ 3DD4B661226C1CEF00451B7B /* BLEUnlock-Bridging-Header.h */, 3D79FD7C226C335100A373C0 /* appleDeviceNames.swift */, 3D2FCF07226C99CB007A06E7 /* Images.xcassets */, + 74D89DC6A5677427FD9C5C11 /* GatewayParsing.swift */, + 5FDCBAD056B2199119AD43EE /* NetworkMonitor.swift */, ); path = BLEUnlock; sourceTree = ""; @@ -130,6 +165,7 @@ 3DD4B667226C1E3700451B7B /* Frameworks */ = { isa = PBXGroup; children = ( + 3D2CF85A255102B300157996 /* MediaRemote.framework */, 3DD4B668226C1E3700451B7B /* login.framework */, ); name = Frameworks; @@ -163,6 +199,7 @@ 3DD4B64D226C1C3200451B7B /* Frameworks */, 3DD4B64E226C1C3200451B7B /* Resources */, 3D600A5922701E5D0068FB7B /* CopyFiles */, + 3D7C0F8022769F1F005A4C43 /* ShellScript */, ); buildRules = ( ); @@ -180,11 +217,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1020; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Takeshi Sone"; TargetAttributes = { 3D600A3F22701A5A0068FB7B = { CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1020; SystemCapabilities = { com.apple.GameCenter.Mac = { enabled = 0; @@ -219,6 +257,12 @@ en, Base, ja, + "zh-Hans", + de, + da, + nb, + sv, + tr, ); mainGroup = 3DD4B647226C1C3200451B7B; productRefGroup = 3DD4B651226C1C3200451B7B /* Products */; @@ -236,7 +280,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3D600A4822701A5C0068FB7B /* MainMenu.xib in Resources */, + 3D4EE4152279E94B00AF9E93 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -246,19 +290,40 @@ files = ( 3D2FCF08226C99CB007A06E7 /* Images.xcassets in Resources */, 3D600A37226CC1A40068FB7B /* Localizable.strings in Resources */, + 3D953797227853E20017D8B9 /* AboutBox.xib in Resources */, 3DD4B659226C1C3400451B7B /* MainMenu.xib in Resources */, - 3DC0BC6D22729050001145EC /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 3D7C0F8022769F1F005A4C43 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/bash; + shellScript = "buildNumber=$(/usr/libexec/PlistBuddy -c \"Print CFBundleVersion\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\")\nbuildNumber=$(($buildNumber + 1))\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 3D600A3C22701A5A0068FB7B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3D600A4322701A5B0068FB7B /* AppDelegate.swift in Sources */, + 3D4EE4112279E55A00AF9E93 /* AppDelegate.m in Sources */, + 3D4EE4132279E56900AF9E93 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -269,7 +334,12 @@ 3DD4B654226C1C3200451B7B /* AppDelegate.swift in Sources */, 3DD4B666226C1DF200451B7B /* BLE.swift in Sources */, 3DD4B664226C1CF000451B7B /* lowlevel.c in Sources */, + 3DCAAA7D22ADE9830011A2DA /* checkUpdate.swift in Sources */, + 3D95379522784D550017D8B9 /* AboutBox.swift in Sources */, + 3D6070E226DBC68E00EE4E48 /* LEDeviceInfo.swift in Sources */, 3D79FD7D226C335100A373C0 /* appleDeviceNames.swift in Sources */, + 8E6BAB7636D41F51AE698213 /* GatewayParsing.swift in Sources */, + FCA22F473FD28EC39CD35151 /* NetworkMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -281,25 +351,29 @@ children = ( 3D600A36226CC1A40068FB7B /* Base */, 3D600A38226CC1AA0068FB7B /* ja */, + B1EFF91622E010F50010DB0A /* zh-Hans */, + 3D43D0122765FCCB00D15991 /* de */, + 3D43D0142765FCFD00D15991 /* da */, + 3D43D0162765FD1600D15991 /* nb */, + 3D43D0182765FD2800D15991 /* sv */, + F749D28D277515C600C78C28 /* tr */, ); name = Localizable.strings; sourceTree = ""; }; - 3D600A4622701A5C0068FB7B /* MainMenu.xib */ = { + 3D953799227853E20017D8B9 /* AboutBox.xib */ = { isa = PBXVariantGroup; children = ( - 3D600A4722701A5C0068FB7B /* Base */, + 3D953798227853E20017D8B9 /* Base */, + 3D95379B227853EC0017D8B9 /* ja */, + B1EFF91422E010F50010DB0A /* zh-Hans */, + 3D43D0112765FCCB00D15991 /* de */, + 3D43D0132765FCFD00D15991 /* da */, + 3D43D0152765FD1600D15991 /* nb */, + 3D43D0172765FD2800D15991 /* sv */, + F749D28C277515C600C78C28 /* tr */, ); - name = MainMenu.xib; - sourceTree = ""; - }; - 3DC0BC6F22729050001145EC /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 3DC0BC6E22729050001145EC /* ja */, - 3DC0BC7022729055001145EC /* en */, - ); - name = InfoPlist.strings; + name = AboutBox.xib; sourceTree = ""; }; 3DD4B657226C1C3400451B7B /* MainMenu.xib */ = { @@ -316,10 +390,11 @@ 3D600A4C22701A5C0068FB7B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = "Mac Developer"; - CODE_SIGN_STYLE = Automatic; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 376NR42P9A; + DEVELOPMENT_TEAM = 42LGPQYC7M; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Launcher/Info.plist; INSTALL_PATH = ""; @@ -327,10 +402,10 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = jp.sone.BLEUnlock.Launcher; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; @@ -338,11 +413,12 @@ 3D600A4D22701A5C0068FB7B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = "Mac Developer"; - CODE_SIGN_STYLE = Automatic; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - DEVELOPMENT_TEAM = 376NR42P9A; + DEVELOPMENT_TEAM = 42LGPQYC7M; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Launcher/Info.plist; INSTALL_PATH = ""; @@ -350,7 +426,6 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = jp.sone.BLEUnlock.Launcher; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -385,6 +460,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -447,13 +523,14 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -478,14 +555,13 @@ 3DD4B65F226C1C3400451B7B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = BLEUnlock/BLEUnlock.entitlements; - CODE_SIGN_IDENTITY = "Mac Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 376NR42P9A; + DEVELOPMENT_TEAM = 42LGPQYC7M; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = BLEUnlock/Info.plist; @@ -493,6 +569,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MARKETING_VERSION = ""; PRODUCT_BUNDLE_IDENTIFIER = jp.sone.BLEUnlock; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -509,14 +586,13 @@ 3DD4B660226C1C3400451B7B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = BLEUnlock/BLEUnlock.entitlements; - CODE_SIGN_IDENTITY = "Mac Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 376NR42P9A; + DEVELOPMENT_TEAM = 42LGPQYC7M; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = BLEUnlock/Info.plist; @@ -524,6 +600,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MARKETING_VERSION = ""; PRODUCT_BUNDLE_IDENTIFIER = jp.sone.BLEUnlock; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/BLEUnlock.xcodeproj/xcshareddata/xcschemes/BLEUnlock.xcscheme b/BLEUnlock.xcodeproj/xcshareddata/xcschemes/BLEUnlock.xcscheme new file mode 100644 index 00000000..0227bea0 --- /dev/null +++ b/BLEUnlock.xcodeproj/xcshareddata/xcschemes/BLEUnlock.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BLEUnlock.xcodeproj/xcuserdata/ts1.xcuserdatad/xcschemes/xcschememanagement.plist b/BLEUnlock.xcodeproj/xcuserdata/ts1.xcuserdatad/xcschemes/xcschememanagement.plist index 88e50b68..641b2f7a 100644 --- a/BLEUnlock.xcodeproj/xcuserdata/ts1.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/BLEUnlock.xcodeproj/xcuserdata/ts1.xcuserdatad/xcschemes/xcschememanagement.plist @@ -19,11 +19,6 @@ SuppressBuildableAutocreation - 3D600A3F22701A5A0068FB7B - - primary - - 3DD4B64F226C1C3200451B7B primary diff --git a/BLEUnlock/AboutBox.swift b/BLEUnlock/AboutBox.swift new file mode 100644 index 00000000..86fe9d4f --- /dev/null +++ b/BLEUnlock/AboutBox.swift @@ -0,0 +1,42 @@ +import Cocoa + +private var aboutBox: AboutBox? = nil + +class AboutBox: NSWindowController, NSWindowDelegate { + @IBOutlet weak var versionLabel: NSTextField! + + @IBAction func visitHomepage(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "https://github.com/ts1/BLEUnlock#readme")!) + } + + @IBAction func checkReleases(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "https://github.com/ts1/BLEUnlock/releases")!) + } + convenience init() { + self.init(windowNibName: "AboutBox") + } + + override func windowDidLoad() { + super.windowDidLoad() + if let info = Bundle.main.infoDictionary { + if let version = info["CFBundleShortVersionString"] as? String { + if let build = info["CFBundleVersion"] as? String { + versionLabel.stringValue = versionLabel.stringValue.replacingOccurrences(of: "#{version}", with: "\(version) (\(build))") + } + } + } + } + + override func cancelOperation(_ sender: Any?) { + close() + } + + static func showAboutBox() { + if (aboutBox == nil) { + aboutBox = AboutBox() + } + aboutBox?.showWindow(nil) + NSApp.activate(ignoringOtherApps: true) + aboutBox?.window?.orderFront(self) + } +} diff --git a/BLEUnlock/AppDelegate.swift b/BLEUnlock/AppDelegate.swift index 455a1d4a..79663fe1 100644 --- a/BLEUnlock/AppDelegate.swift +++ b/BLEUnlock/AppDelegate.swift @@ -2,42 +2,68 @@ import Cocoa import Quartz import ServiceManagement -var appDelegate: AppDelegate? = nil - -@_cdecl("onDisplayWake") -func onDisplayWake() { - appDelegate?.onWake() -} - -@_cdecl("onDisplaySleep") -func onDisplaySleep() { - appDelegate?.onSleep() -} - func t(_ key: String) -> String { return NSLocalizedString(key, comment: "") } @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate { +class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemValidation, NSUserNotificationCenterDelegate, BLEDelegate { let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let ble = BLE() + let networkMonitor = NetworkMonitor() let mainMenu = NSMenu() let deviceMenu = NSMenu() - let proximityMenu = NSMenu() + let lockRSSIMenu = NSMenu() + let unlockRSSIMenu = NSMenu() + let timeoutMenu = NSMenu() + let lockDelayMenu = NSMenu() var deviceDict: [UUID: NSMenuItem] = [:] var monitorMenuItem : NSMenuItem? let prefs = UserDefaults.standard - var sleeping = false + var displaySleep = false + var systemSleep = false var connected = false var userNotification: NSUserNotification? - + var nowPlayingWasPlaying = false + var aboutBox: AboutBox? = nil + var wakeTimer: Timer? + var manualLock = false + var unlockedAt = 0.0 + var inScreensaver = false + var lastRSSI: Int? = nil + var pausedByNetwork = true + var disableOnNetworkMenuItem: NSMenuItem? + func menuWillOpen(_ menu: NSMenu) { if menu == deviceMenu { ble.startScanning() - } else if menu == proximityMenu { + } else if menu == lockRSSIMenu { + for item in menu.items { + if item.tag == ble.lockRSSI { + item.state = .on + } else { + item.state = .off + } + } + } else if menu == unlockRSSIMenu { + for item in menu.items { + if item.tag == ble.unlockRSSI { + item.state = .on + } else { + item.state = .off + } + } + } else if menu == timeoutMenu { + for item in menu.items { + if item.tag == Int(ble.signalTimeout) { + item.state = .on + } else { + item.state = .off + } + } + } else if menu == lockDelayMenu { for item in menu.items { - if item.tag == ble.proximityRSSI { + if item.tag == Int(ble.proximityTimeout) { item.state = .on } else { item.state = .off @@ -45,6 +71,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate } } } + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if menuItem.menu == lockRSSIMenu { + return menuItem.tag <= ble.unlockRSSI + } else if menuItem.menu == unlockRSSIMenu { + return menuItem.tag >= ble.lockRSSI + } + return true + } func menuDidClose(_ menu: NSMenu) { if menu == deviceMenu { @@ -53,13 +88,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate } func menuItemTitle(device: Device) -> String { - return String(format: "%@ (%ddBm)", device.description, device.rssi) + var desc : String! + if let mac = device.macAddr { + let prettifiedMac = mac.replacingOccurrences(of: "-", with: ":").uppercased() + desc = String(format: "%@ (%@)", device.description, prettifiedMac) + } else { + desc = device.description + } + return String(format: "%@ (%ddBm)", desc, device.rssi) } func newDevice(device: Device) { let menuItem = deviceMenu.addItem(withTitle: menuItemTitle(device: device), action:#selector(selectDevice), keyEquivalent: "") deviceDict[device.uuid] = menuItem - if (device.uuid == ble.monitorUUID) { + if (device.uuid == ble.monitoredUUID) { menuItem.state = .on } } @@ -77,15 +119,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate deviceDict.removeValue(forKey: device.uuid) } - func updateRSSI(rssi: Int?) { + func updateRSSI(rssi: Int?, active: Bool) { if let r = rssi { - monitorMenuItem?.title = String(format:"%ddBm", r) + lastRSSI = r + monitorMenuItem?.title = String(format:"%ddBm", r) + (active ? " (Active)" : "") if (!connected) { connected = true statusItem.button?.image = NSImage(named: "StatusBarConnected") } } else { - monitorMenuItem?.title = t("Not detected") + monitorMenuItem?.title = t("not_detected") if (connected) { connected = false statusItem.button?.image = NSImage(named: "StatusBarDisconnected") @@ -93,87 +136,240 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate } } + func bluetoothPowerWarn() { + errorModal(t("bluetooth_power_warn")) + } + func notifyUser(_ reason: String) { let un = NSUserNotification() un.title = "BLEUnlock" if reason == "lost" { - un.subtitle = t("notification_lock_reason_lost_signal") + un.subtitle = t("notification_lost_signal") } else if reason == "away" { - un.subtitle = t("notification_lock_reason_device_away") + un.subtitle = t("notification_device_away") } - un.informativeText = t("notification_title_locked") + un.informativeText = t("notification_locked") un.deliveryDate = Date().addingTimeInterval(1) NSUserNotificationCenter.default.scheduleNotification(un) userNotification = un } + + func userNotificationCenter(_ center: NSUserNotificationCenter, + shouldPresent notification: NSUserNotification) -> Bool { + return true + } + + func userNotificationCenter(_ center: NSUserNotificationCenter, + didActivate notification: NSUserNotification) { + if notification != userNotification { + NSWorkspace.shared.open(URL(string: "https://github.com/ts1/BLEUnlock/releases")!) + NSUserNotificationCenter.default.removeDeliveredNotification(notification) + } + } + + func runScript(_ arg: String) { + guard let directory = try? FileManager.default.url(for: .applicationScriptsDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { return } + let file = directory.appendingPathComponent("event") + let process = Process() + process.executableURL = file + if let r = lastRSSI { + process.arguments = [arg, String(r)] + } else { + process.arguments = [arg] + } + try? process.run() + } + + func pauseNowPlaying() { + guard prefs.bool(forKey: "pauseItunes") else { return } + MRMediaRemoteGetNowPlayingApplicationIsPlaying( + DispatchQueue.main, + { (playing) in + self.nowPlayingWasPlaying = playing + if self.nowPlayingWasPlaying { + print("pause") + MRMediaRemoteSendCommand(MRCommandPause, nil) + } + } + ) + } + func playNowPlaying() { + guard prefs.bool(forKey: "pauseItunes") else { return } + if nowPlayingWasPlaying { + print("play") + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in + MRMediaRemoteSendCommand(MRCommandPlay, nil) + self.nowPlayingWasPlaying = false + }) + } + } + + func lockOrSaveScreen() { + if prefs.bool(forKey: "screensaver") { + NSWorkspace.shared.launchApplication("ScreenSaverEngine") + } else { + if SACLockScreenImmediate() != 0 { + print("Failed to lock screen") + } + if prefs.bool(forKey: "sleepDisplay") { + print("sleep display") + sleepDisplay() + } + } + } + func updatePresence(presence: Bool, reason: String) { + guard !pausedByNetwork else { return } if presence { - if let un = userNotification { - NSUserNotificationCenter.default.removeDeliveredNotification(un) - userNotification = nil - } - if prefs.bool(forKey: "wakeOnProximity") && sleeping { - print("Waking display") - wakeDisplay() + if ble.unlockRSSI != ble.UNLOCK_DISABLED { + if let un = userNotification { + NSUserNotificationCenter.default.removeDeliveredNotification(un) + userNotification = nil + } + if displaySleep && !systemSleep && prefs.bool(forKey: "wakeOnProximity") { + print("Waking display") + wakeDisplay() + wakeTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in + print("Retrying waking display") + wakeDisplay() + }) + } + tryUnlockScreen() } - unlockScreen() } else { - if lockScreen() { - self.notifyUser(reason) - } else { - debugPrint("Failed to lock") + if (!isScreenLocked() && ble.lockRSSI != ble.LOCK_DISABLED) { + pauseNowPlaying() + lockOrSaveScreen() + notifyUser(reason) + runScript(reason) } + manualLock = false } } func fakeKeyStrokes(_ string: String) { let src = CGEventSource(stateID: .hidSystemState) - let pressEvent = CGEvent(keyboardEventSource: src, virtualKey: 49, keyDown: true) - let len = string.count - let buffer = UnsafeMutablePointer.allocate(capacity: len) - NSString(string:string).getCharacters(buffer) - pressEvent?.keyboardSetUnicodeString(stringLength: len, unicodeString: buffer) - pressEvent?.post(tap: .cghidEventTap) - CGEvent(keyboardEventSource: src, virtualKey: 49, keyDown: false)?.post(tap: .cghidEventTap) + // Send 20 characters per keyboard event. That seems to be the limit. + let PER = 20 + let uniCharCount = string.utf16.count + var strIndex = string.utf16.startIndex + for offset in stride(from: 0, to: uniCharCount, by: PER) { + let pressEvent = CGEvent(keyboardEventSource: src, virtualKey: 49, keyDown: true) + let len = offset + PER < uniCharCount ? PER : uniCharCount - offset + let buffer = UnsafeMutablePointer.allocate(capacity: len) + for i in 0.. Bool { + if let dict = CGSessionCopyCurrentDictionary() as? [String : Any] { + if let locked = dict["CGSSessionScreenIsLocked"] as? Int { + return locked == 1 } } + return false } + + func tryUnlockScreen() { + guard !pausedByNetwork else { return } + guard !manualLock else { return } + guard ble.presence else { return } + guard ble.unlockRSSI != ble.UNLOCK_DISABLED else { return } + guard !systemSleep else { return } + guard !displaySleep else { return } - func onWake() { - print("awake") - sleeping = false - Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false, block: { _ in - if self.ble.presence { - self.unlockScreen() - } + if inScreensaver { + // In screensaver, make sure Login panel is displayed + let src = CGEventSource(stateID: .hidSystemState) + // Esc key down and up + CGEvent(keyboardEventSource: src, virtualKey: 0x35, keyDown: true)?.post(tap: .cghidEventTap) + CGEvent(keyboardEventSource: src, virtualKey: 0x35, keyDown: false)?.post(tap: .cghidEventTap) + } + + guard !self.prefs.bool(forKey: "wakeWithoutUnlocking") else { return } + + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in + guard self.isScreenLocked() else { return } + guard let password = self.fetchPassword(warn: true) else { return } + + print("Entering password") + self.unlockedAt = Date().timeIntervalSince1970 + self.fakeKeyStrokes(password) + self.playNowPlaying() + self.runScript("unlocked") }) } - - func onSleep() { - sleeping = true - print("sleep") + + @objc func onDisplayWake() { + print("display wake") + //unlockedAt = Date().timeIntervalSince1970 + displaySleep = false + wakeTimer?.invalidate() + wakeTimer = nil + tryUnlockScreen() + } + + @objc func onDisplaySleep() { + print("display sleep") + displaySleep = true + } + + @objc func onSystemWake() { + print("system wake") + Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in + print("delayed system wake job") + NSApp.setActivationPolicy(.accessory) // Hide Dock icon again + self.systemSleep = false + self.tryUnlockScreen() + }) } + @objc func onSystemSleep() { + print("system sleep") + systemSleep = true + // Set activation policy to regular, so the CBCentralManager can scan for peripherals + // when the Bluetooth will become on again. + // This enables Dock icon but the screen is off anyway. + NSApp.setActivationPolicy(.regular) + } + + @objc func onUnlock() { + Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + print("onUnlock") + if Date().timeIntervalSince1970 >= self.unlockedAt + 10 { + if self.ble.unlockRSSI != self.ble.UNLOCK_DISABLED { + self.runScript("intruded") + } + self.playNowPlaying() + } + }) + manualLock = false + Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in + checkUpdate() + }) + } + + @objc func onScreensaverStart() { + print("screensaver start") + inScreensaver = true + } + + @objc func onScreensaverStop() { + print("screensaver stop") + inScreensaver = false + } + @objc func selectDevice(item: NSMenuItem) { for (uuid, menuItem) in deviceDict { if menuItem == item { @@ -185,9 +381,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate } } } - + func monitorDevice(uuid: UUID) { - monitorMenuItem?.title = t("Not detected") + connected = false + statusItem.button?.image = NSImage(named: "StatusBarDisconnected") + monitorMenuItem?.title = t("not_detected") ble.startMonitor(uuid: uuid) } @@ -195,6 +393,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate let alert = NSAlert() alert.messageText = msg alert.informativeText = info ?? "" + alert.window.title = "BLEUnlock" + NSApp.activate(ignoringOtherApps: true) alert.runModal() } @@ -217,7 +417,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate } } - func fetchPassword() -> String? { + func fetchPassword(warn: Bool = false) -> String? { let query: [String: Any] = [ String(kSecClass): kSecClassGenericPassword, String(kSecAttrAccount): NSUserName(), @@ -230,6 +430,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate let status = SecItemCopyMatching(query as CFDictionary, &item) if (status == errSecItemNotFound) { print("Password is not stored") + if warn { + errorModal(t("password_not_set")) + } return nil } guard status == errSecSuccess else { @@ -246,14 +449,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate @objc func askPassword() { let msg = NSAlert() - msg.addButton(withTitle: "OK") - msg.addButton(withTitle: t("Cancel")) - msg.messageText = t("enter password") - msg.informativeText = t("password is safe") - + msg.addButton(withTitle: t("ok")) + msg.addButton(withTitle: t("cancel")) + msg.messageText = t("enter_password") + msg.informativeText = t("password_info") + msg.window.title = "BLEUnlock" + let txt = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 20)) msg.accessoryView = txt txt.becomeFirstResponder() + NSApp.activate(ignoringOtherApps: true) let response = msg.runModal() if (response == .alertFirstButtonReturn) { @@ -261,6 +466,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate storePassword(pw) } } + + @objc func setRSSIThreshold() { + let msg = NSAlert() + msg.addButton(withTitle: t("ok")) + msg.addButton(withTitle: t("cancel")) + msg.messageText = t("enter_rssi_threshold") + msg.informativeText = t("enter_rssi_threshold_info") + msg.window.title = "BLEUnlock" + + let txt = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 20)) + txt.placeholderString = String(ble.thresholdRSSI) + msg.accessoryView = txt + txt.becomeFirstResponder() + NSApp.activate(ignoringOtherApps: true) + let response = msg.runModal() + + if (response == .alertFirstButtonReturn) { + let val = txt.intValue + ble.thresholdRSSI = Int(val) + prefs.set(val, forKey: "thresholdRSSI") + } + } @objc func toggleWakeOnProximity(_ menuItem: NSMenuItem) { let value = !prefs.bool(forKey: "wakeOnProximity") @@ -268,10 +495,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate prefs.set(value, forKey: "wakeOnProximity") } - @objc func setProximity(_ menuItem: NSMenuItem) { + @objc func setLockRSSI(_ menuItem: NSMenuItem) { + let value = menuItem.tag + prefs.set(value, forKey: "lockRSSI") + ble.lockRSSI = value + } + + @objc func setUnlockRSSI(_ menuItem: NSMenuItem) { let value = menuItem.tag - prefs.set(value, forKey: "proximity") - ble.proximityRSSI = value + prefs.set(value, forKey: "unlockRSSI") + ble.unlockRSSI = value + } + + @objc func setTimeout(_ menuItem: NSMenuItem) { + let value = menuItem.tag + prefs.set(value, forKey: "timeout") + ble.signalTimeout = Double(value) + } + + @objc func setLockDelay(_ menuItem: NSMenuItem) { + let value = menuItem.tag + prefs.set(value, forKey: "lockDelay") + ble.proximityTimeout = Double(value) } @objc func toggleLaunchAtLogin(_ menuItem: NSMenuItem) { @@ -281,41 +526,172 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate SMLoginItemSetEnabled(Bundle.main.bundleIdentifier! + ".Launcher" as CFString, launchAtLogin) } + @objc func togglePauseNowPlaying(_ menuItem: NSMenuItem) { + let pauseNowPlaying = !prefs.bool(forKey: "pauseItunes") + prefs.set(pauseNowPlaying, forKey: "pauseItunes") + menuItem.state = pauseNowPlaying ? .on : .off + } + + @objc func toggleUseScreensaver(_ menuItem: NSMenuItem) { + let value = !prefs.bool(forKey: "screensaver") + prefs.set(value, forKey: "screensaver") + menuItem.state = value ? .on : .off + } + + @objc func toggleSleepDisplay(_ menuItem: NSMenuItem) { + let value = !prefs.bool(forKey: "sleepDisplay") + prefs.set(value, forKey: "sleepDisplay") + menuItem.state = value ? .on : .off + } + + @objc func togglePassiveMode(_ menuItem: NSMenuItem) { + let passiveMode = !prefs.bool(forKey: "passiveMode") + prefs.set(passiveMode, forKey: "passiveMode") + menuItem.state = passiveMode ? .on : .off + ble.setPassiveMode(passiveMode) + } + + @objc func toggleDisableOnThisNetwork(_ menuItem: NSMenuItem) { + // Gateway resolution blocks (route/ping/arp); do it off the main queue. + DispatchQueue.global(qos: .userInitiated).async { + guard let mac = self.networkMonitor.resolveGatewayMAC() else { + DispatchQueue.main.async { self.errorModal(t("network_not_identified")) } + return + } + DispatchQueue.main.async { + var list = self.networkMonitor.allowlist + if list.contains(mac) { + list.remove(mac) + } else { + list.insert(mac) + } + self.networkMonitor.allowlist = list + self.networkMonitor.networkChanged() // recompute pause state now + } + } + } + + @objc func toggleWakeWithoutUnlocking(_ menuItem: NSMenuItem) { + let wakeWithoutUnlocking = !prefs.bool(forKey: "wakeWithoutUnlocking") + prefs.set(wakeWithoutUnlocking, forKey: "wakeWithoutUnlocking") + menuItem.state = wakeWithoutUnlocking ? .on : .off + } + + @objc func lockNow() { + guard !isScreenLocked() else { return } + manualLock = true + pauseNowPlaying() + lockOrSaveScreen() + } + + @objc func showAboutBox() { + AboutBox.showAboutBox() + } + + func constructRSSIMenu(_ menu: NSMenu, _ action: Selector) { + menu.addItem(withTitle: t("closer"), action: nil, keyEquivalent: "") + for proximity in stride(from: -30, to: -100, by: -5) { + let item = menu.addItem(withTitle: String(format: "%ddBm", proximity), action: action, keyEquivalent: "") + item.tag = proximity + } + menu.addItem(withTitle: t("farther"), action: nil, keyEquivalent: "") + menu.delegate = self + } + func constructMenu() { - monitorMenuItem = mainMenu.addItem(withTitle: t("Device not set"), action: nil, keyEquivalent: "") - mainMenu.addItem(NSMenuItem.separator()) + monitorMenuItem = mainMenu.addItem(withTitle: t("device_not_set"), action: nil, keyEquivalent: "") var item: NSMenuItem - item = mainMenu.addItem(withTitle: t("Device"), action: nil, keyEquivalent: "") + + item = mainMenu.addItem(withTitle: t("lock_now"), action: #selector(lockNow), keyEquivalent: "") + mainMenu.addItem(NSMenuItem.separator()) + + item = mainMenu.addItem(withTitle: t("device"), action: nil, keyEquivalent: "") item.submenu = deviceMenu deviceMenu.delegate = self - deviceMenu.addItem(withTitle: t("Scanning..."), action: nil, keyEquivalent: "") - - let proximityItem = mainMenu.addItem(withTitle: t("Proximity RSSI"), action: nil, keyEquivalent: "") - proximityItem.submenu = proximityMenu - proximityMenu.addItem(withTitle: t("⬆Closer"), action: nil, keyEquivalent: "") - for proximity in stride(from: -50, to: -100, by: -10) { - let item = proximityMenu.addItem(withTitle: String(format: "%ddBm", proximity), action: #selector(setProximity), keyEquivalent: "") - item.tag = proximity - } - proximityMenu.addItem(withTitle: t("⬇Farther"), action: nil, keyEquivalent: "") - proximityMenu.delegate = self - - item = mainMenu.addItem(withTitle: t("Wake on proximity"), action: #selector(toggleWakeOnProximity), keyEquivalent: "") + deviceMenu.addItem(withTitle: t("scanning"), action: nil, keyEquivalent: "") + + let unlockRSSIItem = mainMenu.addItem(withTitle: t("unlock_rssi"), action: nil, keyEquivalent: "") + unlockRSSIItem.submenu = unlockRSSIMenu + item = unlockRSSIMenu.addItem(withTitle: t("disabled"), action: #selector(setUnlockRSSI), keyEquivalent: "") + item.tag = ble.UNLOCK_DISABLED + constructRSSIMenu(unlockRSSIMenu, #selector(setUnlockRSSI)) + + let lockRSSIItem = mainMenu.addItem(withTitle: t("lock_rssi"), action: nil, keyEquivalent: "") + lockRSSIItem.submenu = lockRSSIMenu + constructRSSIMenu(lockRSSIMenu, #selector(setLockRSSI)) + item = lockRSSIMenu.addItem(withTitle: t("disabled"), action: #selector(setLockRSSI), keyEquivalent: "") + item.tag = ble.LOCK_DISABLED + + let lockDelayItem = mainMenu.addItem(withTitle: t("lock_delay"), action: nil, keyEquivalent: "") + lockDelayItem.submenu = lockDelayMenu + lockDelayMenu.addItem(withTitle: "2 " + t("seconds"), action: #selector(setLockDelay), keyEquivalent: "").tag = 2 + lockDelayMenu.addItem(withTitle: "5 " + t("seconds"), action: #selector(setLockDelay), keyEquivalent: "").tag = 5 + lockDelayMenu.addItem(withTitle: "15 " + t("seconds"), action: #selector(setLockDelay), keyEquivalent: "").tag = 15 + lockDelayMenu.addItem(withTitle: "30 " + t("seconds"), action: #selector(setLockDelay), keyEquivalent: "").tag = 30 + lockDelayMenu.addItem(withTitle: "1 " + t("minute"), action: #selector(setLockDelay), keyEquivalent: "").tag = 60 + lockDelayMenu.addItem(withTitle: "2 " + t("minutes"), action: #selector(setLockDelay), keyEquivalent: "").tag = 120 + lockDelayMenu.addItem(withTitle: "5 " + t("minutes"), action: #selector(setLockDelay), keyEquivalent: "").tag = 300 + lockDelayMenu.delegate = self + + let timeoutItem = mainMenu.addItem(withTitle: t("timeout"), action: nil, keyEquivalent: "") + timeoutItem.submenu = timeoutMenu + timeoutMenu.addItem(withTitle: "30 " + t("seconds"), action: #selector(setTimeout), keyEquivalent: "").tag = 30 + timeoutMenu.addItem(withTitle: "1 " + t("minute"), action: #selector(setTimeout), keyEquivalent: "").tag = 60 + timeoutMenu.addItem(withTitle: "2 " + t("minutes"), action: #selector(setTimeout), keyEquivalent: "").tag = 120 + timeoutMenu.addItem(withTitle: "5 " + t("minutes"), action: #selector(setTimeout), keyEquivalent: "").tag = 300 + timeoutMenu.addItem(withTitle: "10 " + t("minutes"), action: #selector(setTimeout), keyEquivalent: "").tag = 600 + timeoutMenu.delegate = self + + item = mainMenu.addItem(withTitle: t("wake_on_proximity"), action: #selector(toggleWakeOnProximity), keyEquivalent: "") if prefs.bool(forKey: "wakeOnProximity") { item.state = .on } - mainMenu.addItem(withTitle: t("Set password..."), action: #selector(askPassword), keyEquivalent: "") - item = mainMenu.addItem(withTitle: t("Launch at login"), action: #selector(toggleLaunchAtLogin), keyEquivalent: "") + item = mainMenu.addItem(withTitle: t("wake_without_unlocking"), action: #selector(toggleWakeWithoutUnlocking), keyEquivalent: "") + if prefs.bool(forKey: "wakeWithoutUnlocking") { + item.state = .on + } + + item = mainMenu.addItem(withTitle: t("pause_now_playing"), action: #selector(togglePauseNowPlaying), keyEquivalent: "") + if prefs.bool(forKey: "pauseItunes") { + item.state = .on + } + + item = mainMenu.addItem(withTitle: t("use_screensaver_to_lock"), action: #selector(toggleUseScreensaver), keyEquivalent: "") + if prefs.bool(forKey: "screensaver") { + item.state = .on + } + + item = mainMenu.addItem(withTitle: t("sleep_display"), action: #selector(toggleSleepDisplay), keyEquivalent: "") + if prefs.bool(forKey: "sleepDisplay") { + item.state = .on + } + + mainMenu.addItem(withTitle: t("set_password"), action: #selector(askPassword), keyEquivalent: "") + + item = mainMenu.addItem(withTitle: t("passive_mode"), action: #selector(togglePassiveMode), keyEquivalent: "") + item.state = prefs.bool(forKey: "passiveMode") ? .on : .off + + disableOnNetworkMenuItem = mainMenu.addItem(withTitle: t("disable_on_this_network"), + action: #selector(toggleDisableOnThisNetwork), + keyEquivalent: "") + disableOnNetworkMenuItem?.state = networkMonitor.paused ? .on : .off + + item = mainMenu.addItem(withTitle: t("launch_at_login"), action: #selector(toggleLaunchAtLogin), keyEquivalent: "") item.state = prefs.bool(forKey: "launchAtLogin") ? .on : .off + + mainMenu.addItem(withTitle: t("set_rssi_threshold"), action: #selector(setRSSIThreshold), + keyEquivalent: "") mainMenu.addItem(NSMenuItem.separator()) - mainMenu.addItem(withTitle: t("Quit BLEUnlock"), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "") + mainMenu.addItem(withTitle: t("about"), action: #selector(showAboutBox), keyEquivalent: "") + mainMenu.addItem(NSMenuItem.separator()) + mainMenu.addItem(withTitle: t("quit"), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "") statusItem.menu = mainMenu } - func checkAccessibility() -> Bool { + func checkAccessibility() { let key = kAXTrustedCheckOptionPrompt.takeRetainedValue() as String if (!AXIsProcessTrustedWithOptions([key: true] as CFDictionary)) { // Sometimes Prompt option above doesn't work. @@ -324,10 +700,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate // "Fn" key down and up CGEvent(keyboardEventSource: src, virtualKey: 63, keyDown: true)?.post(tap: .cghidEventTap) CGEvent(keyboardEventSource: src, virtualKey: 63, keyDown: false)?.post(tap: .cghidEventTap) - return false } - return true - } func applicationDidFinishLaunching(_ aNotification: Notification) { @@ -341,19 +714,59 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, BLEDelegate monitorDevice(uuid: uuid) } } - let proximity = prefs.integer(forKey: "proximity") - if proximity != 0 { - ble.proximityRSSI = proximity + let lockRSSI = prefs.integer(forKey: "lockRSSI") + if lockRSSI != 0 { + ble.lockRSSI = lockRSSI } - appDelegate = self; - setSleepNotification() - - if fetchPassword() == nil { + let unlockRSSI = prefs.integer(forKey: "unlockRSSI") + if unlockRSSI != 0 { + ble.unlockRSSI = unlockRSSI + } + let timeout = prefs.integer(forKey: "timeout") + if timeout != 0 { + ble.signalTimeout = Double(timeout) + } + ble.setPassiveMode(prefs.bool(forKey: "passiveMode")) + let thresholdRSSI = prefs.integer(forKey: "thresholdRSSI") + if thresholdRSSI != 0 { + ble.thresholdRSSI = thresholdRSSI + } + let lockDelay = prefs.integer(forKey: "lockDelay") + if lockDelay != 0 { + ble.proximityTimeout = Double(lockDelay) + } + + NSUserNotificationCenter.default.delegate = self + + let nc = NSWorkspace.shared.notificationCenter; + nc.addObserver(self, selector: #selector(onDisplaySleep), name: NSWorkspace.screensDidSleepNotification, object: nil) + nc.addObserver(self, selector: #selector(onDisplayWake), name: NSWorkspace.screensDidWakeNotification, object: nil) + nc.addObserver(self, selector: #selector(onSystemSleep), name: NSWorkspace.willSleepNotification, object: nil) + nc.addObserver(self, selector: #selector(onSystemWake), name: NSWorkspace.didWakeNotification, object: nil) + + let dnc = DistributedNotificationCenter.default + dnc.addObserver(self, selector: #selector(onUnlock), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil) + dnc.addObserver(self, selector: #selector(onScreensaverStart), name: NSNotification.Name(rawValue: "com.apple.screensaver.didstart"), object: nil) + dnc.addObserver(self, selector: #selector(onScreensaverStop), name: NSNotification.Name(rawValue: "com.apple.screensaver.didstop"), object: nil) + + if ble.unlockRSSI != ble.UNLOCK_DISABLED && !prefs.bool(forKey: "wakeWithoutUnlocking") && fetchPassword() == nil { askPassword() } - if (!checkAccessibility()) { - errorModal(t("no-accessibility"), info: t("allow-accessibility")) + checkAccessibility() + checkUpdate() + + networkMonitor.onPauseStateChange = { [weak self] paused in + guard let self = self else { return } + self.pausedByNetwork = paused + self.disableOnNetworkMenuItem?.state = paused ? .on : .off + print("Network pause state: \(paused)") } + networkMonitor.start() + + // Hide dock icon. + // This is required because we can't have LSUIElement set to true in Info.plist, + // otherwise CBCentralManager.scanForPeripherals won't work. + NSApp.setActivationPolicy(.accessory) } func applicationWillTerminate(_ aNotification: Notification) { diff --git a/BLEUnlock/BLE.swift b/BLEUnlock/BLE.swift index 30034ff4..b37fc88b 100644 --- a/BLEUnlock/BLE.swift +++ b/BLEUnlock/BLE.swift @@ -1,5 +1,30 @@ import Foundation import CoreBluetooth +import Accelerate + +let DeviceInformation = CBUUID(string:"180A") +let ManufacturerName = CBUUID(string:"2A29") +let ModelName = CBUUID(string:"2A24") +let ExposureNotification = CBUUID(string:"FD6F") + +func getMACFromUUID(_ uuid: String) -> String? { + guard let plist = NSDictionary(contentsOfFile: "/Library/Preferences/com.apple.Bluetooth.plist") else { return nil } + guard let cbcache = plist["CoreBluetoothCache"] as? NSDictionary else { return nil } + guard let device = cbcache[uuid] as? NSDictionary else { return nil } + return device["DeviceAddress"] as? String +} + +func getNameFromMAC(_ mac: String) -> String? { + guard let plist = NSDictionary(contentsOfFile: "/Library/Preferences/com.apple.Bluetooth.plist") else { return nil } + guard let devcache = plist["DeviceCache"] as? NSDictionary else { return nil } + guard let device = devcache[mac] as? NSDictionary else { return nil } + if let name = device["Name"] as? String { + let trimmed = name.trimmingCharacters(in: .whitespaces) + if trimmed == "" { return nil } + return trimmed + } + return nil +} class Device: NSObject { let uuid : UUID! @@ -9,9 +34,31 @@ class Device: NSObject { var advData: Data? var rssi: Int = 0 var scanTimer: Timer? + var macAddr: String? + var blName: String? override var description: String { get { + if macAddr == nil || blName == nil { + if let info = getLEDeviceInfoFromUUID(uuid.description) { + blName = info.name + macAddr = info.macAddr + } + } + if macAddr == nil { + macAddr = getMACFromUUID(uuid.description) + } + if let mac = macAddr { + if blName == nil { + blName = getNameFromMAC(mac) + } + if let name = blName { + // If it's just "iPhone" or "iPad", there's a chance we can get the model name in the following code + if name != "iPhone" && name != "iPad" { + return name + } + } + } if let manu = manufacture { if let mod = model { if manu == "Apple Inc." && appleDeviceNames[mod] != nil { @@ -30,77 +77,118 @@ class Device: NSObject { if let mod = model { return mod } + // iBeacon if let adv = advData { - var iBeaconPrefix : [uint16] = [0x004c, 0x01502] - if adv[0...3] == Data(bytes: &iBeaconPrefix, count: 4) { - let major = uint16(adv[20]) << 8 | uint16(adv[21]) - let minor = uint16(adv[22]) << 8 | uint16(adv[23]) - let tx = Int8(bitPattern: adv[24]) - let distance = pow(10, Double(Int(tx) - rssi)/20.0) - let d = String(format:"%.1f", distance) - return "iBeacon [\(major), \(minor)] \(d)m" + if adv.count >= 25 { + var iBeaconPrefix : [uint16] = [0x004c, 0x01502] + if adv[0...3] == Data(bytes: &iBeaconPrefix, count: 4) { + let major = uint16(adv[20]) << 8 | uint16(adv[21]) + let minor = uint16(adv[22]) << 8 | uint16(adv[23]) + let tx = Int8(bitPattern: adv[24]) + let distance = pow(10, Double(Int(tx) - rssi)/20.0) + let d = String(format:"%.1f", distance) + return "iBeacon [\(major), \(minor)] \(d)m" + } } } + if let name = blName { + return name + } + if let mac = macAddr { + return mac // better than uuid + } return uuid.description } } - + init(uuid _uuid: UUID) { uuid = _uuid } } -let DeviceInformation = CBUUID(string:"180A") -let ManufacturerName = CBUUID(string:"2A29") -let ModelName = CBUUID(string:"2A24") - protocol BLEDelegate { func newDevice(device: Device) func updateDevice(device: Device) func removeDevice(device: Device) - func updateRSSI(rssi: Int?) + func updateRSSI(rssi: Int?, active: Bool) func updatePresence(presence: Bool, reason: String) + func bluetoothPowerWarn() } class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { + let UNLOCK_DISABLED = 1 + let LOCK_DISABLED = -100 var centralMgr : CBCentralManager! var devices : [UUID : Device] = [:] var delegate: BLEDelegate? var scanMode = false - var monitorUUID: UUID? + var monitoredUUID: UUID? + var monitoredPeripheral: CBPeripheral? var proximityTimer : Timer? var signalTimer: Timer? var presence = false - var proximityRSSI = -70 - var proximityTimeout = 10.0 - var signalTimeout = 31.0 - + var lockRSSI = -80 + var unlockRSSI = -60 + var proximityTimeout = 5.0 + var signalTimeout = 60.0 + var lastReadAt = 0.0 + var powerWarn = true + var passiveMode = false + var thresholdRSSI = -70 + var latestRSSIs: [Double] = [] + var latestN: Int = 5 + var activeModeTimer : Timer? = nil + var connectionTimer : Timer? = nil + + func scanForPeripherals() { + guard !centralMgr.isScanning else { return } + centralMgr.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) + //print("Start scanning") + } + func startScanning() { scanMode = true - for device in devices.values { - resetScanTimer(device: device) - } + scanForPeripherals() } - + func stopScanning() { scanMode = false - for device in devices.values { - device.scanTimer?.invalidate() + if activeModeTimer != nil { + centralMgr.stopScan() } } - + + func setPassiveMode(_ mode: Bool) { + passiveMode = mode + if passiveMode { + activeModeTimer?.invalidate() + activeModeTimer = nil + if let p = monitoredPeripheral { + centralMgr.cancelPeripheralConnection(p) + } + } + scanForPeripherals() + } + func startMonitor(uuid: UUID) { - monitorUUID = uuid + if let p = monitoredPeripheral { + centralMgr.cancelPeripheralConnection(p) + } + monitoredUUID = uuid proximityTimer?.invalidate() resetSignalTimer() - presence = false + presence = true + monitoredPeripheral = nil + activeModeTimer?.invalidate() + activeModeTimer = nil + scanForPeripherals() } - + func resetSignalTimer() { signalTimer?.invalidate() signalTimer = Timer.scheduledTimer(withTimeInterval: signalTimeout, repeats: false, block: { _ in print("Device is lost") - self.delegate?.updateRSSI(rssi: nil) + self.delegate?.updateRSSI(rssi: nil, active: false) if self.presence { self.presence = false self.delegate?.updatePresence(presence: self.presence, reason: "lost") @@ -110,62 +198,149 @@ class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { RunLoop.main.add(timer, forMode: .common) } } - + func centralManagerDidUpdateState(_ central: CBCentralManager) { - if central.state == .poweredOn { - centralMgr.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]) + switch central.state { + case .poweredOn: + print("Bluetooth powered on") + if activeModeTimer == nil { + scanForPeripherals() + } + powerWarn = false + case .poweredOff: + print("Bluetooth powered off") + presence = false + signalTimer?.invalidate() + signalTimer = nil + if powerWarn { + powerWarn = false + delegate?.bluetoothPowerWarn() + } + default: + break } } + func getEstimatedRSSI(rssi: Int) -> Int { + if latestRSSIs.count >= latestN { + latestRSSIs.removeFirst() + } + latestRSSIs.append(Double(rssi)) + var mean: Double = 0.0 + var sddev: Double = 0.0 + vDSP_normalizeD(latestRSSIs, 1, nil, 1, &mean, &sddev, vDSP_Length(latestRSSIs.count)) + return Int(mean) + } + + func updateMonitoredPeripheral(_ rssi: Int) { + // print(String(format: "rssi: %d", rssi)) + if rssi >= (unlockRSSI == UNLOCK_DISABLED ? lockRSSI : unlockRSSI) && !presence { + print("Device is close") + presence = true + delegate?.updatePresence(presence: presence, reason: "close") + latestRSSIs.removeAll() // Avoid bouncing + } + + let estimatedRSSI = getEstimatedRSSI(rssi: rssi) + delegate?.updateRSSI(rssi: estimatedRSSI, active: activeModeTimer != nil) + + if estimatedRSSI >= (lockRSSI == LOCK_DISABLED ? unlockRSSI : lockRSSI) { + if let timer = proximityTimer { + timer.invalidate() + print("Proximity timer canceled") + proximityTimer = nil + } + } else if presence && proximityTimer == nil { + proximityTimer = Timer.scheduledTimer(withTimeInterval: proximityTimeout, repeats: false, block: { _ in + print("Device is away") + self.presence = false + self.delegate?.updatePresence(presence: self.presence, reason: "away") + self.proximityTimer = nil + }) + RunLoop.main.add(proximityTimer!, forMode: .common) + print("Proximity timer started") + } + resetSignalTimer() + } + + func resetScanTimer(device: Device) { + device.scanTimer?.invalidate() + device.scanTimer = Timer.scheduledTimer(withTimeInterval: signalTimeout, repeats: false, block: { _ in + self.delegate?.removeDevice(device: device) + if let p = device.peripheral { + self.centralMgr.cancelPeripheralConnection(p) + } + self.devices.removeValue(forKey: device.uuid) + }) + if let timer = device.scanTimer { + RunLoop.main.add(timer, forMode: .common) + } + } + + func connectMonitoredPeripheral() { + guard let p = monitoredPeripheral else { return } + + // Idk why but this works like a charm when 'didConnect' won't get called. + // However, this generates warnings in the log. + p.readRSSI() + + guard p.state == .disconnected else { return } + print("Connecting") + centralMgr.connect(p, options: nil) + connectionTimer?.invalidate() + connectionTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: false, block: { _ in + if p.state == .connecting { + print("Connection timeout") + self.centralMgr.cancelPeripheralConnection(p) + } + }) + RunLoop.main.add(connectionTimer!, forMode: .common) + } + + //MARK:- CBCentralManagerDelegate start + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { let rssi = RSSI.intValue > 0 ? 0 : RSSI.intValue - if let uuid = monitorUUID { + if let uuid = monitoredUUID { if peripheral.identifier.description == uuid.description { - delegate?.updateRSSI(rssi: rssi) - if (rssi > proximityRSSI) { - if (!presence) { - print("Device is close") - presence = true - delegate?.updatePresence(presence: presence, reason: "close") - } - if let timer = proximityTimer { - timer.invalidate() - print("Proximity timer canceled") - proximityTimer = nil - } - } else if (presence) { - if proximityTimer == nil { - proximityTimer = Timer.scheduledTimer(withTimeInterval: proximityTimeout, repeats: false, block: { _ in - print("Device is away") - self.presence = false - self.delegate?.updatePresence(presence: self.presence, reason: "away") - self.proximityTimer = nil - }) - if let timer = proximityTimer { - RunLoop.main.add(timer, forMode: .common) - print("Proximity timer started") - } + if monitoredPeripheral == nil { + monitoredPeripheral = peripheral + } + if activeModeTimer == nil { + //print("Discover \(rssi)dBm") + updateMonitoredPeripheral(rssi) + if !passiveMode { + connectMonitoredPeripheral() } } - resetSignalTimer() } } if (scanMode) { + if let uuids = advertisementData["kCBAdvDataServiceUUIDs"] as? [CBUUID] { + for uuid in uuids { + if uuid == ExposureNotification { + //print("Device \(peripheral.identifier) Exposure Notification") + return + } + } + } let dev = devices[peripheral.identifier] var device: Device if (dev == nil) { device = Device(uuid: peripheral.identifier) - device.peripheral = peripheral - device.rssi = rssi - device.advData = advertisementData["kCBAdvDataManufacturerData"] as? Data - devices[peripheral.identifier] = device - central.connect(peripheral, options: nil) - delegate?.newDevice(device: device) + if (rssi >= thresholdRSSI) { + device.peripheral = peripheral + device.rssi = rssi + device.advData = advertisementData["kCBAdvDataManufacturerData"] as? Data + devices[peripheral.identifier] = device + central.connect(peripheral, options: nil) + delegate?.newDevice(device: device) + } } else { device = dev! device.rssi = rssi @@ -174,28 +349,55 @@ class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { resetScanTimer(device: device) } } - - func resetScanTimer(device: Device) { - device.scanTimer?.invalidate() - device.scanTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: { _ in - self.delegate?.removeDevice(device: device) - if let p = device.peripheral { - self.centralMgr.cancelPeripheralConnection(p) - } - self.devices.removeValue(forKey: device.uuid) - }) - if let timer = device.scanTimer { - RunLoop.main.add(timer, forMode: .common) - } - } - + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { peripheral.delegate = self - peripheral.discoverServices([DeviceInformation]) + if scanMode { + peripheral.discoverServices([DeviceInformation]) + } + if peripheral == monitoredPeripheral && !passiveMode { + print("Connected") + connectionTimer?.invalidate() + connectionTimer = nil + peripheral.readRSSI() + } } + + //MARK:CBCentralManagerDelegate end - + //MARK:- CBPeripheralDelegate start + + func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { + guard peripheral == monitoredPeripheral else { return } + let rssi = RSSI.intValue > 0 ? 0 : RSSI.intValue + //print("readRSSI \(rssi)dBm") + updateMonitoredPeripheral(rssi) + lastReadAt = Date().timeIntervalSince1970 + + if activeModeTimer == nil && !passiveMode { + print("Entering active mode") + if !scanMode { + centralMgr.stopScan() + } + activeModeTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { _ in + if Date().timeIntervalSince1970 > self.lastReadAt + 10 { + print("Falling back to passive mode") + self.centralMgr.cancelPeripheralConnection(peripheral) + self.activeModeTimer?.invalidate() + self.activeModeTimer = nil + self.scanForPeripherals() + } else if peripheral.state == .connected { + peripheral.readRSSI() + } else { + self.connectMonitoredPeripheral() + } + }) + RunLoop.main.add(activeModeTimer!, forMode: .common) + } + } + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let services = peripheral.services { @@ -206,7 +408,7 @@ class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { } } } - + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) @@ -236,7 +438,7 @@ class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { device.model = s delegate?.updateDevice(device: device) } - if device.model != nil && device.model != nil { + if device.model != nil && device.model != nil && device.peripheral != monitoredPeripheral { centralMgr.cancelPeripheralConnection(peripheral) } } @@ -249,6 +451,7 @@ class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { { peripheral.discoverServices([DeviceInformation]) } + //MARK:CBPeripheralDelegate end - override init() { super.init() diff --git a/BLEUnlock/BLEUnlock-Bridging-Header.h b/BLEUnlock/BLEUnlock-Bridging-Header.h index 5b7e1380..c4b6a0ea 100644 --- a/BLEUnlock/BLEUnlock-Bridging-Header.h +++ b/BLEUnlock/BLEUnlock-Bridging-Header.h @@ -3,3 +3,4 @@ // #include "lowlevel.h" +#include "MediaRemote.h" diff --git a/BLEUnlock/BLEUnlock.entitlements b/BLEUnlock/BLEUnlock.entitlements index 0c67376e..49ad0bb0 100644 --- a/BLEUnlock/BLEUnlock.entitlements +++ b/BLEUnlock/BLEUnlock.entitlements @@ -1,5 +1,8 @@ - + + com.apple.security.automation.apple-events + + diff --git a/BLEUnlock/Base.lproj/AboutBox.xib b/BLEUnlock/Base.lproj/AboutBox.xib new file mode 100644 index 00000000..3434e16b --- /dev/null +++ b/BLEUnlock/Base.lproj/AboutBox.xib @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +
 +Included icons are derived from SVGs downloaded from materialdesignicons.com. They are originally designed by Google LLC and licensed under Apache License version 2.0. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BLEUnlock/Base.lproj/Localizable.strings b/BLEUnlock/Base.lproj/Localizable.strings index c8e08595..8135a10a 100644 --- a/BLEUnlock/Base.lproj/Localizable.strings +++ b/BLEUnlock/Base.lproj/Localizable.strings @@ -1,12 +1,40 @@ -"enter password" = "Enter password to unlock the lock screen."; -"password is safe" = "Your password will be securely stored in Keychain."; -"notification_title_locked" = "Your Mac has been locked by BLEUnlock"; -"notification_lock_reason_lost_signal" = "Signal is lost"; -"notification_lock_reason_device_away" = "Device is away"; -"error_get_permission" = "Failed to get permission to lock screen"; -"not_permitted" = "Not permitted"; -"error_lock_screen" = "Failed to lock screen"; -"error_unlock_screen" = "Failed to unlock screen"; -"Not detected" = "Signal not detected"; -"no-accessibility" = "BLEUnlock uses Accessibility feature to unlock your Mac."; -"allow-accessibility" = "Please allow BLEUnlock to access Accessibility."; +"about" = "About BLEUnlock"; +"bluetooth_power_warn" = "Bluetooth is turned off. For BLEUnlock to work, turn it on."; +"cancel" = "Cancel"; +"closer" = "⬆Closer"; +"device" = "Device"; +"device_not_set" = "Device not selected"; +"disabled" = "Disable"; +"enter_password" = "Enter password to unlock the lock screen."; +"enter_rssi_threshold" = "Enter Minimum RSSI"; +"enter_rssi_threshold_info" = "Device whose RSSI is less than this value will be ignored while scanning."; +"farther" = "⬇Farther"; +"launch_at_login" = "Launch at Login"; +"lock_delay" = "Delay to Lock"; +"lock_now" = "Lock Screen Now"; +"lock_rssi" = "Lock RSSI"; +"minute" = "minute"; +"minutes" = "minutes"; +"not_detected" = "Signal not detected"; +"notification_device_away" = "Device is away"; +"notification_locked" = "This computer has been locked by BLEUnlock"; +"notification_lost_signal" = "Signal is lost"; +"notification_update_available" = "Update is available."; +"ok" = "OK"; +"passive_mode" = "Passive Mode"; +"password_info" = "Your password will be securely stored in Keychain."; +"password_not_set" = "Password is not set."; +"pause_now_playing" = "Pause \"Now Playing\" while Locked"; +"quit" = "Quit BLEUnlock"; +"scanning" = "Scanning…"; +"seconds" = "seconds"; +"set_password" = "Set Password…"; +"set_rssi_threshold" = "Set Minimum RSSI…"; +"sleep_display" = "Turn Off Screen on Lock"; +"timeout" = "No-Signal Timeout"; +"unlock_rssi" = "Unlock RSSI"; +"use_screensaver_to_lock" = "Use Screensaver to Lock"; +"wake_on_proximity" = "Wake on Proximity"; +"wake_without_unlocking" = "Wake without Unlocking"; +"disable_on_this_network" = "Disable on This Network"; +"network_not_identified" = "Could not identify the current network (no IPv4 gateway found)."; diff --git a/BLEUnlock/Base.lproj/MainMenu.xib b/BLEUnlock/Base.lproj/MainMenu.xib index 336d4490..def5847a 100644 --- a/BLEUnlock/Base.lproj/MainMenu.xib +++ b/BLEUnlock/Base.lproj/MainMenu.xib @@ -1,6 +1,7 @@ + @@ -10,7 +11,7 @@ - + diff --git a/BLEUnlock/GatewayParsing.swift b/BLEUnlock/GatewayParsing.swift new file mode 100644 index 00000000..2cf26a8c --- /dev/null +++ b/BLEUnlock/GatewayParsing.swift @@ -0,0 +1,48 @@ +import Foundation + +/// Extracts the IPv4 default-gateway address from `route -n get default` output. +func parseDefaultGatewayIP(routeOutput: String) -> String? { + for line in routeOutput.split(separator: "\n") { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.hasPrefix("gateway:") { + let value = trimmed.dropFirst("gateway:".count).trimmingCharacters(in: .whitespaces) + return value.isEmpty ? nil : value + } + } + return nil +} + +/// Normalises a MAC to lower-case, colon-separated, zero-padded octets. +/// macOS `arp` prints octets without leading zeros (e.g. "a4:2b:8c:1:2:3"), so padding +/// keeps allowlist comparisons stable. Returns nil for anything that is not 6 hex octets. +func normalizeMAC(_ raw: String) -> String? { + let parts = raw.split(separator: ":", omittingEmptySubsequences: false) + guard parts.count == 6 else { return nil } + var octets: [String] = [] + for part in parts { + // The isHexDigit guard is required: Swift's Int(_:radix:) accepts a leading + // '+'/'-', so e.g. "-1" would otherwise parse and emit a malformed octet. + guard (1...2).contains(part.count), + part.allSatisfy({ $0.isHexDigit }), + let value = Int(part, radix: 16) else { return nil } + octets.append(String(format: "%02x", value)) + } + return octets.joined(separator: ":") +} + +/// Extracts and normalises the gateway MAC from `arp -n ` output, e.g. +/// "? (10.0.0.1) at a4:2b:8c:1:2:3 on en0 ifscope [ethernet]". +func parseGatewayMAC(arpOutput: String) -> String? { + let tokens = arpOutput.split(whereSeparator: { $0 == " " || $0 == "\n" || $0 == "\t" }) + guard let atIndex = tokens.firstIndex(of: "at"), atIndex + 1 < tokens.count else { return nil } + let macToken = String(tokens[atIndex + 1]) + if macToken == "(incomplete)" { return nil } + return normalizeMAC(macToken) +} + +/// Pause state with sticky behaviour: an unresolved MAC keeps the previous state, so a +/// transient ARP/route hiccup does not flip the pause off (or on) spuriously. +func computePauseState(resolvedMAC: String?, allowlist: Set, previous: Bool) -> Bool { + guard let mac = resolvedMAC else { return previous } + return allowlist.contains(mac) +} diff --git a/BLEUnlock/Images.xcassets/AboutImage.imageset/Contents.json b/BLEUnlock/Images.xcassets/AboutImage.imageset/Contents.json new file mode 100644 index 00000000..3b5e6e1e --- /dev/null +++ b/BLEUnlock/Images.xcassets/AboutImage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "mac-128x128.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "mac-128x128@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BLEUnlock/Images.xcassets/AboutImage.imageset/mac-128x128.png b/BLEUnlock/Images.xcassets/AboutImage.imageset/mac-128x128.png new file mode 100644 index 00000000..ab8a7ab4 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AboutImage.imageset/mac-128x128.png differ diff --git a/BLEUnlock/Images.xcassets/AboutImage.imageset/mac-128x128@2x.png b/BLEUnlock/Images.xcassets/AboutImage.imageset/mac-128x128@2x.png new file mode 100644 index 00000000..5bc2c9f7 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AboutImage.imageset/mac-128x128@2x.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/Contents.json b/BLEUnlock/Images.xcassets/AppIcon.appiconset/Contents.json index adf23089..b91a0506 100644 --- a/BLEUnlock/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/BLEUnlock/Images.xcassets/AppIcon.appiconset/Contents.json @@ -3,61 +3,61 @@ { "size" : "16x16", "idiom" : "mac", - "filename" : "app-16.png", + "filename" : "mac-16x16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", - "filename" : "app-32.png", + "filename" : "mac-16x16@2x.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", - "filename" : "app-33.png", + "filename" : "mac-32x32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", - "filename" : "app-64.png", + "filename" : "mac-32x32@2x.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", - "filename" : "app-128.png", + "filename" : "mac-128x128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", - "filename" : "app-256.png", + "filename" : "mac-128x128@2x.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", - "filename" : "app-257.png", + "filename" : "mac-256x256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", - "filename" : "app-513.png", + "filename" : "mac-256x256@2x.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", - "filename" : "app-512.png", + "filename" : "mac-512x512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", - "filename" : "app-1024.png", + "filename" : "mac-512x512@2x.png", "scale" : "2x" } ], diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-1024.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-1024.png deleted file mode 100644 index 136ac2ec..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-1024.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-128.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-128.png deleted file mode 100644 index 597fa861..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-128.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-16.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-16.png deleted file mode 100644 index b6415b9f..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-16.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-256.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-256.png deleted file mode 100644 index 4e5fae2c..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-256.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-257.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-257.png deleted file mode 100644 index 4e5fae2c..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-257.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-32.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-32.png deleted file mode 100644 index d159a50f..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-32.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-33.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-33.png deleted file mode 100644 index d159a50f..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-33.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-512.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-512.png deleted file mode 100644 index ac4bba24..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-512.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-513.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-513.png deleted file mode 100644 index ac4bba24..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-513.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-64.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-64.png deleted file mode 100644 index 636d3068..00000000 Binary files a/BLEUnlock/Images.xcassets/AppIcon.appiconset/app-64.png and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-128x128.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-128x128.png new file mode 100644 index 00000000..ab8a7ab4 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-128x128.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-128x128@2x.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-128x128@2x.png new file mode 100644 index 00000000..5bc2c9f7 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-128x128@2x.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-16x16.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-16x16.png new file mode 100644 index 00000000..37ce87a6 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-16x16.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-16x16@2x.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-16x16@2x.png new file mode 100644 index 00000000..0b7cdc43 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-16x16@2x.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-256x256.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-256x256.png new file mode 100644 index 00000000..5bc2c9f7 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-256x256.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-256x256@2x.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-256x256@2x.png new file mode 100644 index 00000000..32eb06fc Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-256x256@2x.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-32x32.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-32x32.png new file mode 100644 index 00000000..0b7cdc43 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-32x32.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-32x32@2x.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-32x32@2x.png new file mode 100644 index 00000000..e66d934c Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-32x32@2x.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-512x512.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-512x512.png new file mode 100644 index 00000000..32eb06fc Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-512x512.png differ diff --git a/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-512x512@2x.png b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-512x512@2x.png new file mode 100644 index 00000000..d4e1d954 Binary files /dev/null and b/BLEUnlock/Images.xcassets/AppIcon.appiconset/mac-512x512@2x.png differ diff --git a/BLEUnlock/Images.xcassets/StatusBarConnected.imageset/app.pdf b/BLEUnlock/Images.xcassets/StatusBarConnected.imageset/app.pdf index 71185105..b2559a50 100644 Binary files a/BLEUnlock/Images.xcassets/StatusBarConnected.imageset/app.pdf and b/BLEUnlock/Images.xcassets/StatusBarConnected.imageset/app.pdf differ diff --git a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/Contents.json b/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/Contents.json index fe003bd5..67bf9093 100644 --- a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/Contents.json +++ b/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/Contents.json @@ -2,17 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "lock-gray.pdf" - }, - { - "idiom" : "universal", - "filename" : "lock-gray-1.pdf", - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ] + "filename" : "unlock-off.pdf" } ], "info" : { @@ -20,7 +10,7 @@ "author" : "xcode" }, "properties" : { - "template-rendering-intent" : "original", + "template-rendering-intent" : "template", "preserves-vector-representation" : true } } \ No newline at end of file diff --git a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/lock-gray-1.pdf b/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/lock-gray-1.pdf deleted file mode 100644 index e9efb1a5..00000000 Binary files a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/lock-gray-1.pdf and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/lock-gray.pdf b/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/lock-gray.pdf deleted file mode 100644 index c32b61fe..00000000 Binary files a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/lock-gray.pdf and /dev/null differ diff --git a/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/unlock-off.pdf b/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/unlock-off.pdf new file mode 100644 index 00000000..1d30b3a2 Binary files /dev/null and b/BLEUnlock/Images.xcassets/StatusBarDisconnected.imageset/unlock-off.pdf differ diff --git a/BLEUnlock/Info.plist b/BLEUnlock/Info.plist index c17b005f..a8713454 100644 --- a/BLEUnlock/Info.plist +++ b/BLEUnlock/Info.plist @@ -17,17 +17,17 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.12.2 CFBundleVersion - 6 + 796 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) - LSUIElement - + NSBluetoothAlwaysUsageDescription + BLEUnlock uses Bluetooth to detect devices. NSHumanReadableCopyright - Copyright © 2019 Takeshi Sone. All rights reserved. + Copyright © 2019-2022 Takeshi Sone. MIT Licensed. NSMainNibFile MainMenu NSPrincipalClass diff --git a/BLEUnlock/LEDeviceInfo.swift b/BLEUnlock/LEDeviceInfo.swift new file mode 100644 index 00000000..adc1e697 --- /dev/null +++ b/BLEUnlock/LEDeviceInfo.swift @@ -0,0 +1,90 @@ +// Resolve MAC address and device name of BLE device from SQLite database at /Library/Bluetooth introduced in Monterey. + +import SQLite3 + +private var inited = false +private var db_paired: OpaquePointer? +private var db_other: OpaquePointer? + +private func connect() { + if inited { return } + + if sqlite3_open("/Library/Bluetooth/com.apple.MobileBluetooth.ledevices.paired.db", &db_paired) == SQLITE_OK { + print("paired.db open success") + } else { + db_paired = nil + } + + if sqlite3_open("/Library/Bluetooth/com.apple.MobileBluetooth.ledevices.other.db", &db_other) == SQLITE_OK { + print("other.db open success") + } else { + db_other = nil + } + + inited = true +} + +struct LEDeviceInfo { + var name: String? + var macAddr: String? +} + +private func getStringFromRow(stmt: OpaquePointer?, index: Int32) -> String? { + if sqlite3_column_type(stmt, index) != SQLITE_TEXT { return nil } + let s = String(cString: sqlite3_column_text(stmt, index)) + let trimmed = s.trimmingCharacters(in: .whitespaces) + if trimmed == "" { return nil } + return trimmed +} + +private func getPairedDeviceFromUUID(_ uuid: String) -> LEDeviceInfo? { + guard let db = db_paired else { return nil } + var stmt: OpaquePointer? + if sqlite3_prepare(db, "SELECT Name, Address, ResolvedAddress FROM PairedDevices where Uuid='\(uuid)'", -1, &stmt, nil) != SQLITE_OK { + print("failed to prepare") + return nil + } + if sqlite3_step(stmt) != SQLITE_ROW { + return nil + } + let name = getStringFromRow(stmt: stmt, index: 0) + let address = getStringFromRow(stmt: stmt, index: 1) + let resolvedAddress = getStringFromRow(stmt: stmt, index: 2) + var mac: String? = nil + if let addr = resolvedAddress ?? address { + // It's like "Public XX:XX:..." or "Random XX:XX:...", so split by space and take the second one + let parts = addr.split(separator: " ") + if parts.count > 1 { + mac = String(parts[1]) + } + } + return LEDeviceInfo(name: name, macAddr: mac) +} + +private func getOtherDeviceFromUUID(_ uuid: String) -> LEDeviceInfo? { + guard let db = db_other else { return nil } + var stmt: OpaquePointer? + if sqlite3_prepare(db, "SELECT Name, Address FROM OtherDevices where Uuid='\(uuid)'", -1, &stmt, nil) != SQLITE_OK { + print("failed to prepare") + return nil + } + if sqlite3_step(stmt) != SQLITE_ROW { + return nil + } + let name = getStringFromRow(stmt: stmt, index: 0) + let address = getStringFromRow(stmt: stmt, index: 1) + var mac: String? = nil + if let addr = address { + // It's like "Public XX:XX:..." or "Random XX:XX:...", so split by space and take the second one + let parts = addr.split(separator: " ") + if parts.count > 1 { + mac = String(parts[1]) + } + } + return LEDeviceInfo(name: name, macAddr: mac) +} + +func getLEDeviceInfoFromUUID(_ uuid: String) -> LEDeviceInfo? { + connect() + return getPairedDeviceFromUUID(uuid) ?? getOtherDeviceFromUUID(uuid); +} diff --git a/BLEUnlock/MediaRemote.h b/BLEUnlock/MediaRemote.h new file mode 100644 index 00000000..0999d757 --- /dev/null +++ b/BLEUnlock/MediaRemote.h @@ -0,0 +1,10 @@ +#include + +typedef enum { + MRCommandPlay, + MRCommandPause, +} MRCommand; + +typedef void (^MRMediaRemoteGetNowPlayingApplicationIsPlayingCompletion)(Boolean isPlaying); +void MRMediaRemoteGetNowPlayingApplicationIsPlaying(dispatch_queue_t queue, MRMediaRemoteGetNowPlayingApplicationIsPlayingCompletion completion); +Boolean MRMediaRemoteSendCommand(MRCommand command, id userInfo); diff --git a/BLEUnlock/NetworkMonitor.swift b/BLEUnlock/NetworkMonitor.swift new file mode 100644 index 00000000..4149bb38 --- /dev/null +++ b/BLEUnlock/NetworkMonitor.swift @@ -0,0 +1,104 @@ +import Foundation +import SystemConfiguration + +/// Watches the default network and decides whether BLEUnlock should be paused on the +/// current ("home") network. The network is identified permission-free by the default +/// gateway's MAC address (route -> ping -> arp). No CoreWLAN, no Location permission. +final class NetworkMonitor { + static let allowlistKey = "pausedGatewayMACs" + + /// Invoked on the MAIN queue whenever the computed pause state changes. + var onPauseStateChange: ((Bool) -> Void)? + + private let prefs = UserDefaults.standard + private let workQueue = DispatchQueue(label: "com.ts1.BLEUnlock.networkmonitor", qos: .utility) + private var store: SCDynamicStore? + + /// Main-queue only. + /// Seeded to `true` (paused) so that during the asynchronous first gateway resolution + /// at launch — route/ping/arp can take a second or more — auto-lock/unlock stays + /// suppressed. The first resolution relaxes it to `false` only once the gateway MAC is + /// confirmed NOT in the allowlist; an unresolvable network keeps it paused (sticky). + private(set) var paused = true + + var allowlist: Set { + get { Set(prefs.stringArray(forKey: NetworkMonitor.allowlistKey) ?? []) } + set { prefs.set(Array(newValue).sorted(), forKey: NetworkMonitor.allowlistKey) } + } + + /// Begin watching for default-route changes and evaluate the current network once. + func start() { + var context = SCDynamicStoreContext(version: 0, + info: Unmanaged.passUnretained(self).toOpaque(), + retain: nil, release: nil, copyDescription: nil) + let callback: SCDynamicStoreCallBack = { _, _, info in + guard let info = info else { return } + Unmanaged.fromOpaque(info).takeUnretainedValue().networkChanged() + } + guard let store = SCDynamicStoreCreate(nil, "BLEUnlock.NetworkMonitor" as CFString, callback, &context) else { + print("NetworkMonitor: failed to create SCDynamicStore") + return + } + let keys = ["State:/Network/Global/IPv4", "State:/Network/Global/IPv6"] as CFArray + // These are exact keys, so they belong in the `keys` argument (not `patterns`). + SCDynamicStoreSetNotificationKeys(store, keys, nil) + if let source = SCDynamicStoreCreateRunLoopSource(nil, store, 0) { + CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes) + } + self.store = store + networkChanged() // initial evaluation + } + + /// Re-evaluate pause state off the main queue, publish back on the main queue. + /// Safe to call from the main queue (the SCDynamicStore callback and start() do). + func networkChanged() { + let allow = allowlist + let previous = paused + workQueue.async { [weak self] in + guard let self = self else { return } + let mac = self.resolveGatewayMAC() + let newPaused = computePauseState(resolvedMAC: mac, allowlist: allow, previous: previous) + DispatchQueue.main.async { + guard newPaused != self.paused else { return } + self.paused = newPaused + self.onPauseStateChange?(newPaused) + } + } + } + + /// Resolve the current default gateway's MAC. BLOCKING — never call on the main queue. + func resolveGatewayMAC() -> String? { + guard let route = runProcess("/sbin/route", ["-n", "get", "default"]), + let gatewayIP = parseDefaultGatewayIP(routeOutput: route) else { + return nil + } + _ = runProcess("/sbin/ping", ["-c", "2", "-t", "1", gatewayIP]) // prime ARP cache + for attempt in 0..<3 { + if let out = runProcess("/usr/sbin/arp", ["-n", gatewayIP]), + let mac = parseGatewayMAC(arpOutput: out) { + return mac + } + if attempt < 2 { usleep(150_000) } // let ARP settle, then retry + } + return nil + } + + @discardableResult + private func runProcess(_ path: String, _ args: [String]) -> String? { + let process = Process() + process.executableURL = URL(fileURLWithPath: path) + process.arguments = args + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = Pipe() + do { + try process.run() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + return String(data: data, encoding: .utf8) + } catch { + print("NetworkMonitor: failed to run \(path): \(error)") + return nil + } + } +} diff --git a/BLEUnlock/appleDeviceNames.swift b/BLEUnlock/appleDeviceNames.swift index 5f37f12c..04d38235 100644 --- a/BLEUnlock/appleDeviceNames.swift +++ b/BLEUnlock/appleDeviceNames.swift @@ -1,92 +1,165 @@ -let appleDeviceNames = [ - "iPhone1,1": "iPhone", - "iPhone1,2": "iPhone 3G", - "iPhone2,1": "iPhone 3GS", - "iPhone3,1": "iPhone 4", - "iPhone3,2": "iPhone 4", - "iPhone3,3": "iPhone 4", - "iPhone4,1": "iPhone 4S", - "iPhone5,1": "iPhone 5", - "iPhone5,2": "iPhone 5", - "iPhone5,3": "iPhone 5c", - "iPhone5,4": "iPhone 5c", - "iPhone6,1": "iPhone 5s", - "iPhone6,2": "iPhone 5s", - "iPhone7,2": "iPhone 6", - "iPhone7,1": "iPhone 6 Plus", - "iPhone8,1": "iPhone 6s", - "iPhone8,2": "iPhone 6s Plus", - "iPhone8,4": "iPhone SE", - "iPhone9,1": "iPhone 7", - "iPhone9,3": "iPhone 7", - "iPhone9,2": "iPhone 7 Plus", - "iPhone9,4": "iPhone 7 Plus", - "iPhone10,1": "iPhone 8", - "iPhone10,4": "iPhone 8", - "iPhone10,2": "iPhone 8 Plus", - "iPhone10,5": "iPhone 8 Plus", - "iPhone10,3": "iPhone X", - "iPhone10,6": "iPhone X", - "iPhone11,8": "iPhone XR", - "iPhone11,2": "iPhone XS", - "iPhone11,6": "iPhone XS Max", - "iPad1,1": "iPad", - "iPad2,1": "iPad 2", - "iPad2,2": "iPad 2", - "iPad2,3": "iPad 2", - "iPad2,4": "iPad 2", - "iPad3,1": "iPad 3", - "iPad3,2": "iPad 3", - "iPad3,3": "iPad 3", - "iPad3,4": "iPad 4", - "iPad3,5": "iPad 4", - "iPad3,6": "iPad 4", - "iPad4,1": "iPad Air", - "iPad4,2": "iPad Air", - "iPad4,3": "iPad Air", - "iPad5,3": "iPad Air 2", - "iPad5,4": "iPad Air 2", - "iPad6,7": "iPad Pro (12.9 inch)", - "iPad6,8": "iPad Pro (12.9 inch)", - "iPad6,3": "iPad Pro (9.7 inch)", - "iPad6,4": "iPad Pro (9.7 inch)", - "iPad6,11": "iPad (5th generation)", - "iPad6,12": "iPad (5th generation)", - "iPad7,1": "iPad Pro (12.9-inch, 2nd generation)", - "iPad7,2": "iPad Pro (12.9-inch, 2nd generation)", - "iPad7,3": "iPad Pro (10.5-inch)", - "iPad7,4": "iPad Pro (10.5-inch)", - "iPad7,5": "iPad (6th generation)", - "iPad7,6": "iPad (6th generation)", - "iPad2,5": "iPad mini", - "iPad2,6": "iPad mini", - "iPad2,7": "iPad mini", - "iPad4,4": "iPad mini 2", - "iPad4,5": "iPad mini 2", - "iPad4,6": "iPad mini 2", - "iPad4,7": "iPad mini 3", - "iPad4,8": "iPad mini 3", - "iPad4,9": "iPad mini 3", - "iPad5,1": "iPad mini 4", - "iPad5,2": "iPad mini 4", - "iPod1,1": "iPod touch", - "iPod2,1": "iPod touch 2G", - "iPod3,1": "iPod touch 3G", - "iPod4,1": "iPod touch 4G", - "iPod5,1": "iPod touch 5G", - "iPod7,1": "iPod touch 6G", - "Watch1,1": "Apple Watch (1st gen.)", - "Watch1,2": "Apple Watch (1st gen.)", - "Watch2,6": "Apple Watch Series 1", - "Watch2,7": "Apple Watch Series 1", - "Watch2,3": "Apple Watch Series 2", - "Watch2,4": "Apple Watch Series 2", - "Watch3,1": "Apple Watch Series 3", - "Watch3,2": "Apple Watch Series 3", - "Watch3,3": "Apple Watch Series 3", - "Watch3,4": "Apple Watch Series 3", - "Watch4,1": "Apple Watch Series 4", - "Watch4,2": "Apple Watch Series 4", - "Watch4,3": "Apple Watch Series 4", - "Watch4,4": "Apple Watch Series 4", -] +let appleDeviceNames = [ + "iPhone1,1": "iPhone", + "iPhone1,2": "iPhone 3G", + "iPhone2,1": "iPhone 3GS", + "iPhone3,1": "iPhone 4 (GSM)", + "iPhone3,2": "iPhone 4 (GSM Rev A)", + "iPhone3,3": "iPhone 4 (CDMA)", + "iPhone4,1": "iPhone 4S", + "iPhone5,1": "iPhone 5", + "iPhone5,2": "iPhone 5", + "iPhone5,3": "iPhone 5c", + "iPhone5,4": "iPhone 5c", + "iPhone6,1": "iPhone 5s", + "iPhone6,2": "iPhone 5s", + "iPhone7,1": "iPhone 6 Plus", + "iPhone7,2": "iPhone 6", + "iPhone8,1": "iPhone 6s", + "iPhone8,2": "iPhone 6s Plus", + "iPhone8,4": "iPhone SE", + "iPhone9,1": "iPhone 7", + "iPhone9,3": "iPhone 7", + "iPhone9,2": "iPhone 7 Plus", + "iPhone9,4": "iPhone 7 Plus", + "iPhone10,1": "iPhone 8", + "iPhone10,4": "iPhone 8", + "iPhone10,2": "iPhone 8 Plus", + "iPhone10,5": "iPhone 8 Plus", + "iPhone12,8": "iPhone SE", + "iPhone10,3": "iPhone X", + "iPhone10,6": "iPhone X", + "iPhone11,2": "iPhone XS", + "iPhone11,4": "iPhone XS Max", + "iPhone11,6": "iPhone XS Max", + "iPhone11,8": "iPhone XR", + "iPhone12,3": "iPhone 11 Pro", + "iPhone12,5": "iPhone 11 Pro Max", + "iPhone13,1": "iPhone 12 mini", + "iPhone13,2": "iPhone 12", + "iPhone13,3": "iPhone 12 Pro", + "iPhone13,4": "iPhone 12 Pro Max", + "iPhone12,1": "iPhone 11", + "iPod1,1": "iPod touch (1st generation)", + "iPod2,1": "iPod touch (2nd generation)", + "iPod3,1": "iPod touch (3rd generation)", + "iPod4,1": "iPod touch (4th generation)", + "iPod5,1": "iPod touch (5th generation)", + "iPod7,1": "iPod touch (6th generation)", + "iPod9,1": "iPod touch (7th generation)", + "iPad1,1": "iPad", + "iPad2,1": "iPad 2", + "iPad2,2": "iPad 2 Wi-Fi + 3G (GSM)", + "iPad2,3": "iPad 2 Wi-Fi + 3G (CDMA)", + "iPad2,4": "iPad 2 (Rev A)", + "iPad3,1": "iPad (3rd generation)", + "iPad3,2": "iPad Wi-Fi + 4G (LTE/CDMA)", + "iPad3,3": "iPad Wi-Fi + 4G (LTE/GSM)", + "iPad3,4": "iPad (4th generation)", + "iPad3,5": "iPad (4th generation)", + "iPad3,6": "iPad (4th generation)", + "iPad4,1": "iPad Air", + "iPad4,2": "iPad Air", + "iPad4,3": "iPad Air", + "iPad5,3": "iPad Air 2", + "iPad5,4": "iPad Air 2", + "iPad6,11": "iPad (5th generation)", + "iPad6,12": "iPad (5th generation)", + "iPad11,3": "iPad Air (3rd generation)", + "iPad11,4": "iPad Air (3rd generation)", + "iPad13,1": "iPad Air (4th generation)", + "iPad13,2": "iPad Air (4th generation)", + "iPad7,5": "iPad (6th generation)", + "iPad7,6": "iPad (6th generation)", + "iPad2,5": "iPad mini", + "iPad2,6": "iPad mini", + "iPad2,7": "iPad mini", + "iPad4,4": "iPad mini 2", + "iPad4,5": "iPad mini 2", + "iPad4,6": "iPad mini 2", + "iPad4,7": "iPad mini 3", + "iPad4,8": "iPad mini 3", + "iPad4,9": "iPad mini 3", + "iPad5,1": "iPad mini 4", + "iPad5,2": "iPad mini 4", + "iPad11,1": "iPad mini (5th generation)", + "iPad11,2": "iPad mini (5th generation)", + "iPad6,7": "iPad Pro (12.9-inch)", + "iPad6,8": "iPad Pro (12.9-inch)", + "iPad6,3": "iPad Pro (9.7-inch)", + "iPad6,4": "iPad Pro (9.7-inch)", + "iPad7,1": "iPad Pro (12.9-inch, 2nd generation)", + "iPad7,2": "iPad Pro (12.9-inch, 2nd generation)", + "iPad7,3": "iPad Pro (10.5-inch)", + "iPad7,4": "iPad Pro (10.5-inch)", + "iPad8,1": "iPad Pro (11-inch)", + "iPad8,2": "iPad Pro (11-inch)", + "iPad8,3": "iPad Pro (11-inch)", + "iPad8,4": "iPad Pro (11-inch)", + "iPad8,5": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,6": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,7": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,8": "iPad Pro (12.9-inch) (3rd generation)", + "iPad8,9": "iPad Pro (11-inch) (2nd generation)", + "iPad8,10": "iPad Pro (11-inch) (2nd generation)", + "iPad8,11": "iPad Pro (12.9-inch) (4th generation)", + "iPad8,12": "iPad Pro (12.9-inch) (4th generation)", + "iPad13,4": "iPad Pro (11-inch) (3rd generation)", + "iPad13,5": "iPad Pro (11-inch) (3rd generation)", + "iPad13,6": "iPad Pro (11-inch) (3rd generation)", + "iPad13,7": "iPad Pro (11-inch) (3rd generation)", + "iPad13,8": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,9": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,10": "iPad Pro (12.9-inch) (5th generation)", + "iPad13,11": "iPad Pro (12.9-inch) (5th generation)", + "iPad7,11": "iPad (7th generation)", + "iPad7,12": "iPad (7th generation)", + "iPad11,6": "iPad (8th generation)", + "iPad11,7": "iPad (8th generation)", + "Watch1,1": "Apple Watch 38mm", + "Watch1,2": "Apple Watch 42mm", + "Watch2,6": "Apple Watch Series 1", + "Watch2,7": "Apple Watch Series 1", + "Watch2,3": "Apple Watch Series 2", + "Watch2,4": "Apple Watch Series 2", + "Watch3,1": "Apple Watch Series 3 (GPS + Cellular)", + "Watch3,2": "Apple Watch Series 3 (GPS + Cellular)", + "Watch3,3": "Apple Watch Series 3 (GPS)", + "Watch3,4": "Apple Watch Series 3 (GPS)", + "Watch4,1": "Apple Watch Series 4", + "Watch4,2": "Apple Watch Series 4", + "Watch4,3": "Apple Watch Series 4", + "Watch4,4": "Apple Watch Series 4", + "Watch5,1": "Apple Watch Series 5", + "Watch5,2": "Apple Watch Series 5", + "Watch5,3": "Apple Watch Series 5", + "Watch5,4": "Apple Watch Series 5", + "Watch6,1": "Apple Watch Series 6", + "Watch6,2": "Apple Watch Series 6", + "Watch6,3": "Apple Watch Series 6", + "Watch6,4": "Apple Watch Series 6", + "Watch5,9": "Apple Watch SE", + "Watch5,10": "Apple Watch SE", + "Watch5,11": "Apple Watch SE", + "Watch5,12": "Apple Watch SE", + "AppleTV2,1": "Apple TV (2nd generation)", + "AppleTV3,1": "Apple TV (3rd generation)", + "AppleTV3,2": "Apple TV (3rd generation Rev A)", + "AppleTV5,3": "Apple TV (4th generation)", + "AppleTV6,2": "Apple TV 4K", + "AppleTV11,1": "Apple TV 4K (2nd generation)", + "AudioAccessory1,1": "HomePod", + "AudioAccessory1,2": "HomePod", + "AudioAccessory5,1": "HomePod mini", + "iPad12,1": "iPad (9th generation)", + "iPad12,2": "iPad (9th generation)", + "iPad14,1": "iPad mini (6th generation)", + "iPad14,2": "iPad mini (6th generation)", + "Watch6,6": "Apple Watch Series 7", + "Watch6,7": "Apple Watch Series 7", + "Watch6,8": "Apple Watch Series 7", + "Watch6,9": "Apple Watch Series 7", + "iPhone14,2": "iPhone 13 Pro", + "iPhone14,3": "iPhone 13 Pro Max", + "iPhone14,4": "iPhone 13 mini", + "iPhone14,5": "iPhone 13", +] diff --git a/BLEUnlock/checkUpdate.swift b/BLEUnlock/checkUpdate.swift new file mode 100644 index 00000000..adb38d6c --- /dev/null +++ b/BLEUnlock/checkUpdate.swift @@ -0,0 +1,48 @@ +import Cocoa + +private let KEY = "lastUpdateCheck" +private let INTERVAL = 24.0 * 60 * 60 +private var notified = false +private var lastCheckAt = UserDefaults.standard.double(forKey: KEY) + +func checkUpdate() { + guard !notified else { return } + let now = NSDate().timeIntervalSince1970 + guard now - lastCheckAt >= INTERVAL else { return } + doCheckUpdate() +} + +private func doCheckUpdate() { + var request = URLRequest(url: URL(string: "https://api.github.com/repos/ts1/BLEUnlock/releases/latest")!) + request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in + if let jsondata = data { + if let json = try? JSONSerialization.jsonObject(with: jsondata) { + if let dict = json as? [String:Any] { + if let version = dict["tag_name"] as? String { + lastCheckAt = NSDate().timeIntervalSince1970 + UserDefaults.standard.set(lastCheckAt, forKey: KEY) + compareVersionsAndNotify(version) + } + } + } + } + }) + task.resume() +} + +private func compareVersionsAndNotify(_ latestVersion: String) { + if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { + if version != latestVersion { + notify() + notified = true + } + } +} + +private func notify() { + let un = NSUserNotification() + un.title = "BLEUnlock" + un.subtitle = t("notification_update_available") + NSUserNotificationCenter.default.deliver(un) +} diff --git a/BLEUnlock/da.lproj/AboutBox.strings b/BLEUnlock/da.lproj/AboutBox.strings new file mode 100644 index 00000000..6a5c2958 --- /dev/null +++ b/BLEUnlock/da.lproj/AboutBox.strings @@ -0,0 +1,11 @@ +/* Class = "NSTextFieldCell"; title = "BLEUnlock version #{version}"; ObjectID = "VLW-23-BX0"; */ +"VLW-23-BX0.title" = "BLEUnlock version #{version}"; + +/* Class = "NSButtonCell"; title = "Check Releases"; ObjectID = "WIg-Gy-UeI"; */ +"WIg-Gy-UeI.title" = "Tjek opdateringer"; + +/* Class = "NSButtonCell"; title = "Visit Homepage"; ObjectID = "fPY-L3-Edh"; */ +"fPY-L3-Edh.title" = "Besøg hjemmesiden"; + +/* Class = "NSWindow"; title = "About BLEUnlock"; ObjectID = "hgX-Z1-8qq"; */ +"hgX-Z1-8qq.title" = "Om BLEUnlock"; diff --git a/BLEUnlock/da.lproj/Localizable.strings b/BLEUnlock/da.lproj/Localizable.strings new file mode 100644 index 00000000..ccc259e5 --- /dev/null +++ b/BLEUnlock/da.lproj/Localizable.strings @@ -0,0 +1,38 @@ +"about" = "Om BLEUnlock"; +"bluetooth_power_warn" = "Bluetooth er slået fra. Du skal slå den til for at få BLEUnlock til at virke."; +"cancel" = "Annuller"; +"closer" = "⬆Nærmere"; +"device" = "Enhed"; +"device_not_set" = "Enheden er ikke valgt"; +"disabled" = "Deaktivere"; +"enter_password" = "Indtast adgangskode for at låse låseskærmen op."; +"enter_rssi_threshold" = "Indtast mindste RSSI"; +"enter_rssi_threshold_info" = "Enheder, hvis RSSI er mindre end denne værdi, vil blive ignoreret under scanningen."; +"farther" = "⬇Længere væk"; +"launch_at_login" = "Start ved login"; +"lock_delay" = "Forsinkelse til låsning"; +"lock_now" = "Lås skærm nu"; +"lock_rssi" = "Lås RSSI"; +"minute" = "minut"; +"minutes" = "minutter"; +"not_detected" = "Signal ikke fundet"; +"notification_device_away" = "Enheden er væk"; +"notification_locked" = "Denne computer er blevet låst af BLEUnlock"; +"notification_lost_signal" = "Signalet er tabt"; +"notification_update_available" = "Opdatering er tilgængelig."; +"ok" = "OK"; +"passive_mode" = "Passiv tilstand"; +"password_info" = "Din adgangskode vil blive gemt sikkert i nøglering."; +"password_not_set" = "Der er ikke indstillet et kodeord."; +"pause_now_playing" = "Pause \"Afspilning nu\", mens den er låst"; +"quit" = "Afslut BLEUnlock"; +"scanning" = "Scanning…"; +"seconds" = "seconder"; +"set_password" = "Indstil adgangskode…"; +"set_rssi_threshold" = "Indstil mindste RSSI…"; +"sleep_display" = "Slå skærmen fra ved låsning"; +"timeout" = "Timeout for manglende signal"; +"unlock_rssi" = "Låse op RSSI"; +"use_screensaver_to_lock" = "Brug screensaver til at låse"; +"wake_on_proximity" = "Vågn op på nærhed"; +"wake_without_unlocking" = "Vågn op uden at låse op"; diff --git a/BLEUnlock/de.lproj/AboutBox.strings b/BLEUnlock/de.lproj/AboutBox.strings new file mode 100644 index 00000000..515dfefc --- /dev/null +++ b/BLEUnlock/de.lproj/AboutBox.strings @@ -0,0 +1,4 @@ +"VLW-23-BX0.title" = "BLEUnlock Version #{version}"; +"hgX-Z1-8qq.title" = "Über BLEUnlock"; +"fPY-L3-Edh.title" = "Homepage"; +"WIg-Gy-UeI.title" = "Versionen"; diff --git a/BLEUnlock/de.lproj/Localizable.strings b/BLEUnlock/de.lproj/Localizable.strings new file mode 100644 index 00000000..254e1a01 --- /dev/null +++ b/BLEUnlock/de.lproj/Localizable.strings @@ -0,0 +1,40 @@ +"about" = "Über BLEUnlock"; +"bluetooth_power_warn" = "Bluetooth ist ausgeschalten. Muss eingeschalten sein, damit BLEUnlock funktioniert."; +"cancel" = "Abbruch"; +"closer" = "⬆näher"; +"device" = "Gerät"; +"device_not_set" = "Kein Gerät ausgewählt"; +"disabled" = "Ausgeschalten"; +"enter_password" = "Passwort eingeben, um den Sperrschirm zu entsperren."; +"enter_rssi_threshold" = "Minimum RSSI eingeben"; +"enter_rssi_threshold_info" = "Gerät mit geringerem RSSI als diesem Wert wird beim Scannen ignoriert."; +"farther" = "⬇weiter"; +"launch_at_login" = "Beim Login starten"; +"lock_delay" = "Sperrverzögerung"; +"lock_now" = "Jetzt sperren"; +"lock_rssi" = "RSSI Sperren"; +"minute" = "Minute"; +"minutes" = "Minuten"; +"not_detected" = "Kein Signal entdeckt"; +"notification_device_away" = "Gerät ist entfernt"; +"notification_locked" = "Dieses Gerät wurde durch BLEUnlock gesperrt"; +"notification_lost_signal" = "Signal ging verloren"; +"notification_update_available" = "Update verfügbar"; +"ok" = "OK"; +"passive_mode" = "Passiver Modus"; +"password_info" = "Das Passwort wurde sicher am Schlüsselbund gespeichert."; +"password_not_set" = "Kein Passwort gesetzt."; +"pause_now_playing" = "Pause \"Spielt jetzt\" während der Sperre"; +"quit" = "BLEUnlock beenden"; +"scanning" = "Scanning"; +"seconds" = "seconds"; +"set_password" = "Passwort setzen…"; +"set_rssi_threshold" = "Setze Minimum RSSI…"; +"sleep_display" = "Bildschirm beim Sperren ausschalten"; +"timeout" = "Kein-Signal Zeitablauf"; +"unlock_rssi" = "RSSI entsperren"; +"use_screensaver_to_lock" = "Den Bildschirmschoner verwenden zum Sperren"; +"wake_on_proximity" = "Aufwachen bei Annäherung"; +"wake_without_unlocking" = "Aufwachen ohne Entsperren"; +"disable_on_this_network" = "In diesem Netzwerk deaktivieren"; +"network_not_identified" = "Aktuelles Netzwerk konnte nicht identifiziert werden (kein IPv4-Gateway gefunden)."; diff --git a/BLEUnlock/en.lproj/InfoPlist.strings b/BLEUnlock/en.lproj/InfoPlist.strings deleted file mode 100644 index 655302b2..00000000 --- a/BLEUnlock/en.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"NSAppleEventsUsageDescription" = "BLEUnlock requires permission to lock screen."; diff --git a/BLEUnlock/en.lproj/Localizable.strings b/BLEUnlock/en.lproj/Localizable.strings deleted file mode 100644 index 2ccc1722..00000000 --- a/BLEUnlock/en.lproj/Localizable.strings +++ /dev/null @@ -1,7 +0,0 @@ -/* - Localizable.strings - BLEUnlock - - Created by Takeshi Sone on 2019/04/22. - Copyright © 2019 Takeshi Sone. All rights reserved. -*/ diff --git a/BLEUnlock/en.lproj/MainMenu.strings b/BLEUnlock/en.lproj/MainMenu.strings deleted file mode 100644 index 8b137891..00000000 --- a/BLEUnlock/en.lproj/MainMenu.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/BLEUnlock/ja.lproj/AboutBox.strings b/BLEUnlock/ja.lproj/AboutBox.strings new file mode 100644 index 00000000..e3db5e2f --- /dev/null +++ b/BLEUnlock/ja.lproj/AboutBox.strings @@ -0,0 +1,4 @@ +"VLW-23-BX0.title" = "BLEUnlock バージョン #{version}"; +"hgX-Z1-8qq.title" = "BLEUnlock について"; +"fPY-L3-Edh.title" = "ホームページ"; +"WIg-Gy-UeI.title" = "リリース情報"; diff --git a/BLEUnlock/ja.lproj/InfoPlist.strings b/BLEUnlock/ja.lproj/InfoPlist.strings deleted file mode 100644 index 8f471e84..00000000 --- a/BLEUnlock/ja.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"NSAppleEventsUsageDescription" = "画面をロックするために許可が必要です。"; diff --git a/BLEUnlock/ja.lproj/Localizable.strings b/BLEUnlock/ja.lproj/Localizable.strings index c9fe5c31..39910a7e 100644 --- a/BLEUnlock/ja.lproj/Localizable.strings +++ b/BLEUnlock/ja.lproj/Localizable.strings @@ -1,23 +1,38 @@ -"Device" = "デバイス"; -"Not detected" = "信号未検出"; -"Scanning..." = "スキャン中..."; -"Proximity RSSI" = "信号強度しきい値"; -"⬆Closer" = "⬆近い"; -"⬇Farther" = "⬇遠い"; -"Wake on proximity" = "画面スリープから復帰させる"; -"Set password..." = "パスワードを設定..."; -"Launch at login" = "ログイン時に起動"; -"Quit BLEUnlock" = "BLEUnlockを終了"; -"enter password" = "ロック画面を解除するためのパスワードを入力してください。"; -"password is safe" = "パスワードはキーチェーンに安全に保存されます。"; -"Cancel" = "キャンセル"; -"notification_title_locked" = "MacはBLEUnlockによってロックされました"; -"notification_lock_reason_lost_signal" = "デバイスからの信号がありません"; -"notification_lock_reason_device_away" = "デバイスがMacから離れました"; -"error_get_permission" = "画面をロックする許可が得られませんでした"; -"not_permitted" = "許可がありません"; -"error_lock_screen" = "画面をロックできませんでした"; -"error_unlock_screen" = "画面ロックを解除できませんでした"; -"Device not set" = "デバイス未選択"; -"no-accessibility" = "BLEUnlockはMacのロックを解除するためにアクセシビリティ機能を使います。"; -"allow-accessibility" = "BLEUnlockを許可してください"; +"about" = "BLEUnlock について"; +"bluetooth_power_warn" = "Bluetooth がオフになっています。 BLEUnlock を動作させるためにはオンにしてください。"; +"cancel" = "キャンセル"; +"closer" = "⬆近い"; +"device" = "デバイス"; +"device_not_set" = "デバイス未選択"; +"disabled" = "無効にする"; +"enter_password" = "ロック画面を解除するためのパスワードを入力してください。"; +"enter_rssi_threshold" = "最小RSSIを入力"; +"enter_rssi_threshold_info" = "この値よりRSSIが小さいデバイスはスキャンに表示されません。"; +"farther" = "⬇遠い"; +"launch_at_login" = "ログイン時に起動"; +"lock_delay" = "ロックするまでの遅延"; +"lock_now" = "今すぐロック"; +"lock_rssi" = "ロック信号強度"; +"minute" = "分"; +"minutes" = "分"; +"not_detected" = "信号未検出"; +"notification_device_away" = "デバイスが Mac から離れました"; +"notification_locked" = "Mac は BLEUnlock によってロックされました"; +"notification_lost_signal" = "デバイスからの信号がありません"; +"notification_update_available" = "アップデートが利用可能です"; +"ok" = "OK"; +"passive_mode" = "パッシブモード"; +"password_info" = "パスワードはキーチェーンに安全に保存されます。"; +"password_not_set" = "パスワードが設定されていません。"; +"pause_now_playing" = "ロック中 \"再生中\" を一時停止"; +"quit" = "BLEUnlock を終了"; +"scanning" = "スキャン中…"; +"seconds" = "秒"; +"set_password" = "パスワードを設定…"; +"set_rssi_threshold" = "最小RSSIを設定…"; +"sleep_display" = "ロック時画面をスリープ"; +"timeout" = "無信号タイムアウト"; +"unlock_rssi" = "アンロック信号強度"; +"use_screensaver_to_lock" = "スクリーンセーバーでロック"; +"wake_on_proximity" = "画面スリープから復帰"; +"wake_without_unlocking" = "アンロックせずに画面復帰"; diff --git a/BLEUnlock/ja.lproj/MainMenu.strings b/BLEUnlock/ja.lproj/MainMenu.strings deleted file mode 100644 index 8b137891..00000000 --- a/BLEUnlock/ja.lproj/MainMenu.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/BLEUnlock/lowlevel.c b/BLEUnlock/lowlevel.c index 4eed394a..add5e5b0 100644 --- a/BLEUnlock/lowlevel.c +++ b/BLEUnlock/lowlevel.c @@ -1,63 +1,18 @@ #include "lowlevel.h" - -#include -#include -#include - -#include -#include -#include - #include -#include - -static void -sleepCallback(void *refCon, io_service_t service, natural_t messageType, void *messageArgument) -{ - switch (messageType) { - case kIOMessageDeviceWillPowerOff: - onDisplaySleep(); - break; - case kIOMessageDeviceHasPoweredOn: - onDisplayWake(); - break; - } -} - -void setSleepNotification(void) -{ - io_service_t displayWrangler; - IONotificationPortRef notificationPort; - io_object_t notification; - - displayWrangler = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceNameMatching("IODisplayWrangler")); - notificationPort = IONotificationPortCreate(kIOMasterPortDefault); - IOServiceAddInterestNotification(notificationPort, displayWrangler, kIOGeneralInterest, sleepCallback, NULL, ¬ification); - - CFRunLoopAddSource (CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode); - IOObjectRelease (displayWrangler); -} +#include void wakeDisplay(void) { - IOPMAssertionID assertionID; - IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); + static IOPMAssertionID assertionID; + IOPMAssertionDeclareUserActivity(CFSTR("BLEUnlock"), kIOPMUserActiveLocal, &assertionID); } -bool lockScreen(void) +void sleepDisplay(void) { -#if 1 - // Go to lock screen by private API. Doesn't work in Sandbox. - extern int SACLockScreenImmediate(void); - return SACLockScreenImmediate() == 0; -#else - // Go to display sleep. User must set lock screen immediately after sleep in order to lock. io_registry_entry_t reg = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler"); if (reg) { IORegistryEntrySetCFProperty(reg, CFSTR("IORequestIdle"), kCFBooleanTrue); IOObjectRelease(reg); - return true; } - return false; -#endif } diff --git a/BLEUnlock/lowlevel.h b/BLEUnlock/lowlevel.h index 6860b61b..af1127e7 100644 --- a/BLEUnlock/lowlevel.h +++ b/BLEUnlock/lowlevel.h @@ -2,13 +2,8 @@ #define lowlevel_h #include -void setSleepNotification(void); +void sleepDisplay(void); void wakeDisplay(void); -bool lockScreen(void); - -// callback -void onDisplaySleep(void); -void onDisplayWake(void); - +int SACLockScreenImmediate(void); #endif /* lowlevel_h */ diff --git a/BLEUnlock/nb.lproj/AboutBox.strings b/BLEUnlock/nb.lproj/AboutBox.strings new file mode 100644 index 00000000..c9e46088 --- /dev/null +++ b/BLEUnlock/nb.lproj/AboutBox.strings @@ -0,0 +1,12 @@ + +/* Class = "NSTextFieldCell"; title = "BLEUnlock version #{version}"; ObjectID = "VLW-23-BX0"; */ +"VLW-23-BX0.title" = "BLEUnlock version #{version}"; + +/* Class = "NSButtonCell"; title = "Check Releases"; ObjectID = "WIg-Gy-UeI"; */ +"WIg-Gy-UeI.title" = "Sjekk utgivelser"; + +/* Class = "NSButtonCell"; title = "Visit Homepage"; ObjectID = "fPY-L3-Edh"; */ +"fPY-L3-Edh.title" = "Besøk hjemmesiden"; + +/* Class = "NSWindow"; title = "About BLEUnlock"; ObjectID = "hgX-Z1-8qq"; */ +"hgX-Z1-8qq.title" = "Om BLEUnlock"; diff --git a/BLEUnlock/nb.lproj/Localizable.strings b/BLEUnlock/nb.lproj/Localizable.strings new file mode 100644 index 00000000..37bc754a --- /dev/null +++ b/BLEUnlock/nb.lproj/Localizable.strings @@ -0,0 +1,38 @@ +"about" = "Om BLEUnlock"; +"bluetooth_power_warn" = "Bluetooth er slått av. Slå den på for at BLEUnlock skal fungere."; +"cancel" = "Annullere"; +"closer" = "⬆Nærmere"; +"device" = "Enhet"; +"device_not_set" = "Enheten er ikke valgt"; +"disabled" = "Deaktivere"; +"enter_password" = "Skriv inn passord for å låse opp låseskjermen."; +"enter_rssi_threshold" = "Angi minimum RSSI"; +"enter_rssi_threshold_info" = "Enhet der RSSI er mindre enn denne verdien, ignoreres under skanning."; +"farther" = "⬇Lenger unna"; +"launch_at_login" = "Start ved innlogging"; +"lock_delay" = "Forsinkelse for låsing"; +"lock_now" = "Lås skjerm nå"; +"lock_rssi" = "Lås RSSI"; +"minute" = "minutt"; +"minutes" = "minutn"; +"not_detected" = "Finner ikke signal"; +"notification_device_away" = "Enheten er borte"; +"notification_locked" = "Denne datamaskinen er låst av BLEUnlock"; +"notification_lost_signal" = "Signalet går tapt"; +"notification_update_available" = "Oppdatering er tilgjengelig."; +"ok" = "OK"; +"passive_mode" = "Passiv Modus"; +"password_info" = "Passordet lagres sikkert i nøkkelringen."; +"password_not_set" = "Passord er ikke angitt."; +"pause_now_playing" = "Stans \"Avspilling nå\" mens den er låst"; +"quit" = "Slutte BLEUnlock"; +"scanning" = "kcanning…"; +"seconds" = "seconder"; +"set_password" = "Angi passord…"; +"set_rssi_threshold" = "Angi minimum RSSI…"; +"sleep_display" = "Slå av skjerm ved låsing"; +"timeout" = "Tidsavbrudd uten signal"; +"unlock_rssi" = "Låse opp RSSI"; +"use_screensaver_to_lock" = "Bruke skjermsparer til å låse"; +"wake_on_proximity" = "Vekk ved nærhet"; +"wake_without_unlocking" = "Våkne uten å låse opp"; diff --git a/BLEUnlock/sv.lproj/AboutBox.strings b/BLEUnlock/sv.lproj/AboutBox.strings new file mode 100644 index 00000000..96d18c13 --- /dev/null +++ b/BLEUnlock/sv.lproj/AboutBox.strings @@ -0,0 +1,13 @@ + +/* Class = "NSTextFieldCell"; title = "BLEUnlock version #{version}"; ObjectID = "VLW-23-BX0"; */ +"VLW-23-BX0.title" = "BLEUnlock version #{version}"; + +/* Class = "NSButtonCell"; title = "Check Releases"; ObjectID = "WIg-Gy-UeI"; */ +"WIg-Gy-UeI.title" = "Observera updateringar"; + +/* Class = "NSButtonCell"; title = "Visit Homepage"; ObjectID = "fPY-L3-Edh"; */ +"fPY-L3-Edh.title" = "Besök hemsida"; + +/* Class = "NSWindow"; title = "About BLEUnlock"; ObjectID = "hgX-Z1-8qq"; */ +"hgX-Z1-8qq.title" = "Om BLEUnlock"; + diff --git a/BLEUnlock/sv.lproj/Localizable.strings b/BLEUnlock/sv.lproj/Localizable.strings new file mode 100644 index 00000000..4ca0820a --- /dev/null +++ b/BLEUnlock/sv.lproj/Localizable.strings @@ -0,0 +1,38 @@ +"about" = "Om BLEUnlock"; +"bluetooth_power_warn" = "Bluetooth är avstängt. För att BLEUnlock ska fungera måste du slå på det."; +"cancel" = "Avbryta"; +"closer" = "⬆Närmare"; +"device" = "Enhet"; +"device_not_set" = "Ingen enhet har valts"; +"disabled" = "Inaktivera"; +"enter_password" = "Ange lösenord för att låsa upp skärmen automatiskt."; +"enter_rssi_threshold" = "Ange minsta RSSI värde"; +"enter_rssi_threshold_info" = "Enheter vars RSSI-värde är lägre än detta kommer att ignoreras vid skanning efter en ny enhet."; +"farther" = "⬇Längre bort"; +"launch_at_login" = "Starta vid inloggning"; +"lock_delay" = "Fördröjning för låsning"; +"lock_now" = "Lås skärmen nu"; +"lock_rssi" = "Lås RSSI"; +"minute" = "minut"; +"minutes" = "minuter"; +"not_detected" = "Ingen enhet upptäcktes"; +"notification_device_away" = "Enheten är borta"; +"notification_locked" = "Den här datorn har låsts av BLEUnlock"; +"notification_lost_signal" = "Signalen är förlorad"; +"notification_update_available" = "En updatering finns tillgänglig."; +"ok" = "OK"; +"passive_mode" = "Passivt läge"; +"password_info" = "Ditt lösenord lagras säkert i nyckelringen."; +"password_not_set" = "Inget lösenordet har angetts."; +"pause_now_playing" = "Pausa \"Spela nu\" när låst"; +"quit" = "Avsluta BLEUnlock"; +"scanning" = "Skannar…"; +"seconds" = "sekunder"; +"set_password" = "Ange lösenord…"; +"set_rssi_threshold" = "Ange lägsta RSSI…"; +"sleep_display" = "Stäng av skärmen vid låsning"; +"timeout" = "Ingen-signal tidsgräns"; +"unlock_rssi" = "Upplåsnings RSSI"; +"use_screensaver_to_lock" = "Använd skärmsläckare för att låsa"; +"wake_on_proximity" = "Väck om i närheten"; +"wake_without_unlocking" = "Vakna utan att låsa upp"; diff --git a/BLEUnlock/tr.lproj/AboutBox.strings b/BLEUnlock/tr.lproj/AboutBox.strings new file mode 100644 index 00000000..d607635c --- /dev/null +++ b/BLEUnlock/tr.lproj/AboutBox.strings @@ -0,0 +1,12 @@ + +/* Class = "NSTextFieldCell"; title = "BLEUnlock version #{version}"; ObjectID = "VLW-23-BX0"; */ +"VLW-23-BX0.title" = "BLEUnlock versiyon #{version}"; + +/* Class = "NSButtonCell"; title = "Check Releases"; ObjectID = "WIg-Gy-UeI"; */ +"WIg-Gy-UeI.title" = "Sürüm Kontrol et"; + +/* Class = "NSButtonCell"; title = "Visit Homepage"; ObjectID = "fPY-L3-Edh"; */ +"fPY-L3-Edh.title" = "Anasayfayı ziyaret et"; + +/* Class = "NSWindow"; title = "About BLEUnlock"; ObjectID = "hgX-Z1-8qq"; */ +"hgX-Z1-8qq.title" = "BLEUnlock Hakkında"; diff --git a/BLEUnlock/tr.lproj/Localizable.strings b/BLEUnlock/tr.lproj/Localizable.strings new file mode 100644 index 00000000..1376de24 --- /dev/null +++ b/BLEUnlock/tr.lproj/Localizable.strings @@ -0,0 +1,38 @@ +"about" = "BLEUnlock Hakkında"; +"bluetooth_power_warn" = "Bluetooth Kapalı. BLEUnlock çalışması için Bluetooth u açın"; +"cancel" = "İptal"; +"closer" = "⬆Yakın"; +"device" = "Aygıt"; +"device_not_set" = "Aygıt seçilmedi"; +"disabled" = "Devredışı"; +"enter_password" = "Kilit ekranı açmak için şifrenizi girin"; +"enter_rssi_threshold" = "Minimum RSSI belirle"; +"enter_rssi_threshold_info" = "Eğer aygıt RSSI değeri bu değerin altında ise arama sırasında yok sayılacaktır."; +"farther" = "⬇Uzak"; +"launch_at_login" = "Başlangıçta çalıştır"; +"lock_delay" = "Kilitleme gecikmesi"; +"lock_now" = "Ekranı şimdi kilitle"; +"lock_rssi" = "Kilitleme RSSI değeri"; +"minute" = "dakika"; +"minutes" = "dakika"; +"not_detected" = "Sinyal tespit edilemedi"; +"notification_device_away" = "Aygıt uzakta"; +"notification_locked" = "Bu bilgisayar BLEUnlock tarafından kilitlenmiştir"; +"notification_lost_signal" = "Sinyal kayboldu"; +"notification_update_available" = "Güncelleme var"; +"ok" = "TAMAM"; +"passive_mode" = "Passif Mod"; +"password_info" = "Şifreniz güvenli bir şekilde anahtar zincirinde saklanacaktır."; +"password_not_set" = "Şifre belirlenmedi."; +"pause_now_playing" = "Kilitliyken \"Now Playing\" beklet"; +"quit" = "BLEUnlock'tan çıkış"; +"scanning" = "Taranıyor…"; +"seconds" = "saniye"; +"set_password" = "Şifre belirle…"; +"set_rssi_threshold" = "Minimum RSSI belirle"; +"sleep_display" = "Kilitli Ekranı Kapat"; +"timeout" = "Sinyal yok Zaman Aşımı"; +"unlock_rssi" = "Kilit açma RSSI"; +"use_screensaver_to_lock" = "Kilit ekranı için ekran kuruyucu kullan"; +"wake_on_proximity" = "Uyandırma yakınlığı"; +"wake_without_unlocking" = "Kilidi açmadan uyanın"; diff --git a/BLEUnlock/zh-Hans.lproj/AboutBox.strings b/BLEUnlock/zh-Hans.lproj/AboutBox.strings new file mode 100644 index 00000000..08a046ac --- /dev/null +++ b/BLEUnlock/zh-Hans.lproj/AboutBox.strings @@ -0,0 +1,11 @@ +/* Class = "NSTextFieldCell"; title = "BLEUnlock version #{version}"; ObjectID = "VLW-23-BX0"; */ +"VLW-23-BX0.title" = "BLEUnlock 版本 #{version}"; + +/* Class = "NSButtonCell"; title = "Check Releases"; ObjectID = "WIg-Gy-UeI"; */ +"WIg-Gy-UeI.title" = "版本检查"; + +/* Class = "NSButtonCell"; title = "Visit Homepage"; ObjectID = "fPY-L3-Edh"; */ +"fPY-L3-Edh.title" = "访问主页"; + +/* Class = "NSWindow"; title = "About BLEUnlock"; ObjectID = "hgX-Z1-8qq"; */ +"hgX-Z1-8qq.title" = "关于BLEUnlock"; diff --git a/BLEUnlock/zh-Hans.lproj/Localizable.strings b/BLEUnlock/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..c8a0303f --- /dev/null +++ b/BLEUnlock/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,38 @@ +"about" = "关于BLEUnlock"; +"bluetooth_power_warn" = "蓝牙已关闭。要使BLEUnlock正常工作,请将其打开。"; +"cancel" = "取消"; +"closer" = "⬆接近"; +"device" = "设备列表"; +"device_not_set" = "未选择设备"; +"disabled" = "禁用"; +"enter_password" = "输入密码以解锁电脑。"; +"enter_rssi_threshold" = "输入最小RSSI"; +"enter_rssi_threshold_info" = "扫描设备时会忽略RSSI值低于该值的设备。"; +"farther" = "⬇远离"; +"launch_at_login" = "开机启动"; +"lock_delay" = "延迟锁定"; +"lock_now" = "立刻锁定屏幕"; +"lock_rssi" = "锁定 RSSI"; +"minute" = "分钟"; +"minutes" = "分钟"; +"not_detected" = "未检测到信息"; +"notification_device_away" = "设备掉线"; +"notification_locked" = "本电脑已被BLEUnlock锁定"; +"notification_lost_signal" = "信号丢失"; +"notification_update_available" = "发现新版本。"; +"ok" = "确定"; +"passive_mode" = "被动模式"; +"password_info" = "您的密码将安全地存储在钥匙串中。"; +"password_not_set" = "密码未设置。"; +"pause_now_playing" = "锁定时暂停\"播放中\""; +"quit" = "退出BLEUnlock"; +"scanning" = "扫描中…"; +"seconds" = "秒钟"; +"set_password" = "设置密码…"; +"set_rssi_threshold" = "设置最小RSSI…"; +"sleep_display" = "锁定时关闭屏幕"; +"timeout" = "无信号超时"; +"unlock_rssi" = "解锁 RSSI"; +"use_screensaver_to_lock" = "用屏保来锁定它"; +"wake_on_proximity" = "靠近唤醒"; +"wake_without_unlocking" = "唤醒时不需解锁"; diff --git a/ExportOptions.plist b/ExportOptions.plist index fea2fcac..6bc71d2a 100644 --- a/ExportOptions.plist +++ b/ExportOptions.plist @@ -5,10 +5,8 @@ destination export method - developer-id + mac-application signingStyle automatic - teamID - 376NR42P9A diff --git a/Launcher/AppDelegate.h b/Launcher/AppDelegate.h new file mode 100644 index 00000000..a59935ba --- /dev/null +++ b/Launcher/AppDelegate.h @@ -0,0 +1,4 @@ +#import + +@interface AppDelegate : NSObject +@end diff --git a/Launcher/AppDelegate.m b/Launcher/AppDelegate.m new file mode 100644 index 00000000..384d80dc --- /dev/null +++ b/Launcher/AppDelegate.m @@ -0,0 +1,29 @@ +#import "AppDelegate.h" + +@interface AppDelegate () +@end + +@implementation AppDelegate + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + NSString *id = [[NSBundle mainBundle] bundleIdentifier]; + NSString *mainId = [id stringByReplacingOccurrencesOfString:@".Launcher" withString:@""]; + if ([NSRunningApplication runningApplicationsWithBundleIdentifier:mainId].count > 0) { + [NSApp terminate:self]; + } + + NSString *path = [[NSBundle mainBundle] bundlePath]; + NSMutableArray *components = [NSMutableArray arrayWithArray:[path pathComponents]]; + [components removeLastObject]; + [components removeLastObject]; + [components removeLastObject]; + [components removeLastObject]; + NSString *mainPath = [NSString pathWithComponents:components]; + [[NSWorkspace sharedWorkspace] launchApplication:mainPath]; + [NSApp terminate:self]; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification { +} + +@end diff --git a/Launcher/AppDelegate.swift b/Launcher/AppDelegate.swift deleted file mode 100644 index c3961ba8..00000000 --- a/Launcher/AppDelegate.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Cocoa - -@NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate { - func applicationDidFinishLaunching(_ aNotification: Notification) { - let mainId = Bundle.main.bundleIdentifier!.replacingOccurrences(of: ".Launcher", with: "") - guard NSRunningApplication.runningApplications(withBundleIdentifier: mainId).isEmpty else { - NSApp.terminate(nil) - return - } - let path = Bundle.main.bundlePath as NSString - var components = path.pathComponents - components.removeLast() - components.removeLast() - components.removeLast() - components.removeLast() - let mainPath = NSString.path(withComponents: components) - NSWorkspace.shared.launchApplication(mainPath) - NSApp.terminate(nil) - } -} diff --git a/Launcher/Info.plist b/Launcher/Info.plist index 41120f50..a1b38996 100644 --- a/Launcher/Info.plist +++ b/Launcher/Info.plist @@ -23,7 +23,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2019 Takeshi Sone. All rights reserved. + Copyright © 2019 Takeshi Sone. MIT licecnced. NSMainNibFile MainMenu NSPrincipalClass diff --git a/Launcher/Base.lproj/MainMenu.xib b/Launcher/MainMenu.xib similarity index 88% rename from Launcher/Base.lproj/MainMenu.xib rename to Launcher/MainMenu.xib index 26f73fba..0297fed8 100644 --- a/Launcher/Base.lproj/MainMenu.xib +++ b/Launcher/MainMenu.xib @@ -1,6 +1,7 @@ + @@ -11,7 +12,7 @@ - + diff --git a/Launcher/main.m b/Launcher/main.m new file mode 100644 index 00000000..8a6799b4 --- /dev/null +++ b/Launcher/main.m @@ -0,0 +1,5 @@ +#import + +int main(int argc, const char * argv[]) { + return NSApplicationMain(argc, argv); +} diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 00000000..314da5df --- /dev/null +++ b/README.ja.md @@ -0,0 +1,252 @@ +# BLEUnlock + +![CI](https://github.com/ts1/BLEUnlock/workflows/CI/badge.svg) +![Github All Releases](https://img.shields.io/github/downloads/ts1/BLEUnlock/total.svg) +[![Buy me a coffee](img/buymeacoffee.svg)](https://www.buymeacoffee.com/tsone) + +BLEUnlockはiPhone, Apple Watchやその他のBluetooth Low Energyデバイスの距離によってMacをロック・アンロックする小さなメニューバーユーティリティーです。 + +## 特徴 + +- iPhoneアプリは必要ありません +- 定期的に信号を送出するBLEデバイスなら何でも使えます ([静的なMACアドレス](#macアドレスについて)である必要があります) +- BLEデバイスがMacの近くにあればMacのロック画面を自動的に解除します +- BLEデバイスがMacから離れると画面をロックします +- ロック・アンロック時にスクリプトを実行することができます +- BLEデバイスがMacに近づくと画面スリープを解除することができます +- BLEデバイスがMacから離れる/近づくと音楽や動画の再生を一時停止/再生することができます +- パスワードはキーチェーンに安全に保管されます + +## 必要なもの + +- Bluetooth Low EnergyをサポートするMac +- macOS 10.13 (High Sierra) 以上 +- iPhone 5s以上, Apple Watch (すべて), または定期的に信号を[静的なMACアドレス](#macアドレスについて)から送信するBLEデバイス + +## インストール + +### Homebrew Caskを使う方法 + +``` +$ brew install bleunlock +``` + +### 手動でインストールする方法 + +[Releases](https://github.com/ts1/BLEUnlock/releases)からzipファイルをダウンロードし、解凍してアプリケーションフォルダに移動します。 + +## セットアップ + +初回起動時、以下の許可を要求します。適切に許可してください。 + +許可 | 説明 +---|--- +Bluetooth | 当然ながら、Bluetoothへのアクセスが必要です。 +アクセシビリティ | ロック画面を解除するために必要です。システム環境設定の画面で左下のロックアイコンをクリックしてアンロックし、BLEUnlockをオンにしてください。 +キーチェイン | (要求されない場合もあります) 要求された場合、必ず**常に許可**を選んでください。ロック中に必要になるためです。 +通知 | (任意) BLEUnlockはロック中に通知メッセージを表示します。正しく動作しているか確認するのに役立ちます。Big Sur以降、デフォルトでロック画面ではメッセージが表示されません。メッセージを表示するには*通知*環境設定パネルで*プレビューを表示*を*常に*に設定してください。 + +| 必要になる許可はmacOSのバージョンが上がるにつれ増えています。古いmacOSをお使いの場合は上に挙げた許可が表示されない場合があります。 + +次にあなたのログインパスワードを聞いてきます。これはロック画面を解除するために必要です。キーチェインに安全に保存されます。 + +最後に、メニューバーアイコンから*デバイス*を選択してください。近くにあるBLEデバイスのスキャンが始まります。使いたいデバイスを選べば完了です。 + +## オプション + +### 今すぐロック +BLEデバイスが近くにあるかどうかに関わらず、画面をロックします。BLEデバイスが一度遠ざかり、再び近づくと解除されます。席を離れる前に確実にロックするのに有効です。 + +### アンロック信号強度 +ロック解除に必要なBluetooth信号の強度です。値が大きいほどBLEデバイスがMacの近くにないとロック解除しません。*無効にする*を選ぶとBLEデバイスが近くにあってもロック解除をしません。 + +### ロック信号強度 +MacをロックするBluetooth信号の強度の閾値です。値が小さいほどBLEデバイスがMacから遠い時点でロックをします。*無効にする*を選ぶとBLEデバイスが遠くなるもしくは信号がなくなってもロックをしません。 + +### ロックするまでの遅延 + +BLEデバイスが遠ざかってから実際にロックをするまでの時間です。BLEデバイスがこの時間内に再び近づくと、ロックは行われません。 + +### 無信号タイムアウト + +最後に信号を受信してからロックするまでの時間です。意図せず「デバイスからの信号がありません」でロックされる場合、この値を増やしてください。 + +### 画面スリープから復帰 + +ロック中にBLEデバイスが近づいてきたとき、ディスプレイをスリープ画面から復帰させます。 + +### アンロックせずに画面復帰 + +ディスプレイがスリープから復帰したとき(「画面スリープから復帰」による自動または手動に関わらず)、Macをアンロックしません。 +これはApple WatchやTouch IDなどのよりセキュアなアンロック機構を使用したい場合に、「画面スリープから復帰」と共に使用すると素早く画面にアクセスできます。 + +### ロック中 "再生中" を一時停止 + +ロック時に音楽や動画の再生を一時停止し、ロック解除時に再開します。対応しているのはApple Music, QuickTime Player, Spotifyなど、*再生中*ウィジェットやキーボードの⏯キーで制御できるアプリです。 + +### スクリーンセーバーでロック + +ロック時にスクリーンセーバーを起動します。このオプションが正しく動作するには、*セキュリティとプライバシー*システム環境パネルで*スリープとスクリーンセーバーの解除にパスワードを要求*を*すぐに*に設定する必要があります。 + +### ロック時画面をスリープ + +ロック時にロック画面を表示せずディスプレイをスリープします。 + +### パスワードを設定... + +Macのログインパスワードを変更したときに、このオプションを使って変更してください。 + +### パッシブモード + +デフォルトでBLEUnlockはBLEデバイスに接続を確立し信号強度を読み取ろうとします。これはサポートされているデバイスでは最も安定して信号強度を読み取ることができる方法です。しかしながら、キーボード、マウス、トラックパッドや特にインターネット共有など、他のBluetooth機器を使用している場合、このモードが干渉することがあります。2.4GHz帯のWiFiも干渉する可能性があります。Bluetoothが不安定になる場合は、パッシブモードを有効にしてください。 + +### 最小RSSIを設定 + +このRSSI未満のデバイスはデバイススキャンリストに表示されません。 + +## トラブルシューティング + +### デバイスがリストに表示されない + +Apple製以外のBLEデバイスでは、BLEUnlockはデバイスの名前を取得できない場合があります。その場合、デバイスはUUID(ハイフンで区切られた長い16進数)で表示されます。 + +デバイスを識別するには、Macから遠ざけたり近づけたりして、信号強度(dB値)がそれに応じて変化するかどうかを確認してください。 + +もしリストに何も表示されない場合、下記のBluetoothモジュールのリセットを試してください。 + + +### アンロックされない + +*システム環境設定* > *セキュリティとプライバシー* > *アクセシビリティ* でBLEUnlockがオンになっているか確認してください。すでにオンになっている場合、一度オフにしてもう一度オンにしてみてください。 + +もしキーチェインの許可を求められた場合、*常に許可*を選択してください。ロック中に必要になるためです。 + +### "デバイスからの信号がありません" が頻繁に発生する場合 + +*無信号タイムアウト*を大きくしてください。それでも解決しない場合、*パッシブモード*を試してください。 + +### Bluetoothキーボード、マウス、インターネット共有その他Bluetoothがおかしくなった + +メニューバーもしくはコントロールセンターにあるBluetoothアイコンをShift+Option+クリックし、表示される*Bluetoothモジュールのリセット*をしてみてください。 + +macOS 12 Montereyでは、上記のオプションはなくなっています。 +代わりに、ターミナルで以下のコマンドを入力してBluetoothモジュールをリセットしてください。 + +``` +sudo pkill bluetoothd +``` + +このコマンドは、ログインパスワードを要求します。 + +それでも問題が繰り返し起こる場合、*パッシブモード*をオンにしてください。 + +## MACアドレスについて + +クラシックBluetoothと違い、Bluetooth Low Energyデバイスは*プライベート*MACアドレスを使うことができます。プライベートアドレスはランダムで、時間が経つと変わることがあります。 + +最近のスマートデバイスは、iOSとAndroidともに、15分ほどで変わるランダムアドレスを使う傾向にあります。おそらくトラッキング防止のためだと思われます。 + +一方で、BLEUnlockは、BLEデバイスをトラッキングするために、MACアドレスは静的である必要があります。 + +幸運なことに、Appleのデバイスでは、Macと同じApple IDでサインインしていれば、真の(パブリック)MACアドレスが取得できます。 + +Android等、その他のデバイスに関しては、今のところMACアドレスを解決する方法は分かりません。非Apple製デバイスでMACアドレスが時間が経つと変わる場合、残念ながらBLEUnlockはサポートできません。 + +MACアドレスが正しく解決されているかチェックするには、*デバイス*のリストに表示されるMACアドレスと、BLEデバイスのアドレスを比較してください。 + +## ロック・アンロック時にスクリプトを実行する + +BLEUnlockはロック・アンロック時に以下のスクリプトを実行します。 + +``` +~/Library/Application Scripts/jp.sone.BLEUnlock/event +``` + +スクリプトにはイベントに応じて以下の引数の一つが渡されます。 + +|Event|Argument| +|-----|--------| +|信号強度のためBLEUnlockによりロックされた|`away`| +|無信号のためBLEUnlockによりロックされた|`lost`| +|BLEUnlockによりアンロックされた|`unlocked`| +|手動でアンロックされた|`intruded`| + +> 注意: `intruded` イベントが正常に働くには、システム環境設定の *セキュリティとプライバシー* で *スリープとスクリーンセーバの解除にパスワードを要求* を **すぐに** に設定してください。 + +### サンプル + +例としてLINE Notifyにメッセージを送るスクリプトを示します。 +手動でアンロックされた場合Macの前にいる人の写真を添付します。 + +```sh +#!/bin/bash + +set -eo pipefail + +LINE_TOKEN=xxxxx + +notify() { + local message=$1 + local image=$2 + if [ "$image" ]; then + img_arg="-F imageFile=@$image" + else + img_arg="" + fi + curl -X POST -H "Authorization: Bearer $LINE_TOKEN" -F "message=$message" \ + $img_arg https://notify-api.line.me/api/notify +} + +capture() { + open -Wa SnapshotUnlocker + ls -t /tmp/unlock-*.jpg | head -1 +} + +case $1 in + away) + notify "$(hostname -s) is locked by BLEUnlock because iPhone is away." + ;; + lost) + notify "$(hostname -s) is locked by BLEUnlock because signal is lost." + ;; + unlocked) + #notify "$(hostname -s) is unlocked by BLEUnlock." + ;; + intruded) + notify "$(hostname -s) is manually unlocked." $(capture) + ;; +esac +``` + +`SnapshotUnlocker` はスクリプトエディタで作った .app で、内容は以下のとおりです。 + +``` +do shell script "/usr/local/bin/ffmpeg -f avfoundation -r 30 -i 0 -frames:v 1 -y /tmp/unlock-$(date +%Y%m%d_%H%M%S).jpg" +``` + +このappはBLEUnlockにカメラのパーミッションがないため必要となります。このappにパーミッションを与えることによりパーミッションの問題を回避できます。 + +## FUNDING + +Apple Developer Programの年間費用は、寄付金で賄われています。 + +もしこのアプリを気に入っていただけたら、継続できるよう、[Buy Me a Coffee](https://www.buymeacoffee.com/tsone) もしくは [PayPal.Me](https://paypal.me/takeshisone) で寄付をしていただけるとありがたいです。 + +## クレジット + +- [peiit](https://github.com/peiit): 中国語のローカリゼーション +- [wenmin-wu](https://github.com/wenmin-wu): 最小RSSIと移動平均 +- [stephengroat](https://github.com/stephengroat): CI +- [joeyhoer](https://github.com/joeyhoer): Homebrew Cask +- [Skyearn](https://github.com/Skyearn): Big Surスタイルのアイコン +- [cyberclaus](https://github.com/cyberclaus): ドイツ語, スウェーデン語, ノルウェー語 (Bokmål) およびデンマーク語のローカリゼーション +- [alonewolfx2](https://github.com/alonewolfx2): トルコ語のローカリゼーション +- [wernjie](https://github.com/wernjie): アンロックせずに画面復帰 + +アイコンはmaterialdesignicons.comからダウンロードしたSVGファイルをもとにしています。これらはGoogleによってデザインされApache License version 2.0でライセンスされています。 + +## ライセンス + +MIT + +Copyright © 2019-2022 Takeshi Sone. diff --git a/README.md b/README.md index 436e4437..cedbed62 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,232 @@ # BLEUnlock -Lock/Unlock your Mac with your iPhone, Apple Watch, or any other Bluetooth LE device. +## Please note that I don't distribute this app on the Mac App Store. You can find it here for free! + +![CI](https://github.com/ts1/BLEUnlock/workflows/CI/badge.svg) +![Github All Releases](https://img.shields.io/github/downloads/ts1/BLEUnlock/total.svg) +[![Buy me a coffee](img/buymeacoffee.svg)](https://www.buymeacoffee.com/tsone) + +BLEUnlock is a small menu bar utility that locks and unlocks your Mac by proximity of your iPhone, Apple Watch, or any other Bluetooth Low Energy device. + +This document is also available in [Japanese (日本語版はこちら)](README.ja.md). ## Features - No iPhone app is required -- Works with any BLE devices that periodically send signal -- Locks Mac when the specified device is away from Mac -- Unlocks Mac for you when the device is near Mac, no need to enter password again -- Optionally wakes from display sleep state +- Works with any BLE devices that periodically transmits signal from [static MAC address](#notes-on-mac-address) +- Unlocks your Mac for you when the BLE device is near your Mac, without entering password +- Locks your Mac when the BLE device is away from your Mac +- Optionally runs your own script upon lock/unlock +- Optionally wakes from display sleep +- Optionally pauses and unpauses music/video playback when you're away and back - Password is securely stored in Keychain +## Requirements + +- A Mac with Bluetooth Low Energy support +- macOS 10.13 (High Sierra) or later +- iPhone 5s or newer, Apple Watch (all), or another BLE device that has [static MAC address](#notes-on-mac-address) and transmits signal periodically + ## Installation -Unzip and copy BLEUnlock to Applications folder. +### Using Homebrew Cask + +``` +brew install bleunlock +``` + +### Manual installation + +Download the zip file from [Releases](https://github.com/ts1/BLEUnlock/releases), unzip and move to the Applications folder. + +## Setting up + +On the first launch, it asks for the following permissions, which you must grant: + +Permission | Description +-----------|--- +Bluetooth | Obviously, Bluetooth access is required. Choose *OK*. +Accessibility | This is required to unlock the locked screen. Click *Open System Preferences*, click the lock icon on the bottom left to unlock, and turn on BLEUnlock. +Keychain | (Not always asked) If asked, you have to choose **Always Allow** because it is required while the screen is locked. +Notification | (Optional) BLEUnlock shows a message on the lock screen when it locks the screen. It is helpful to know if it's working properly. Additionally, to see the message on the lock screen, you need to set *Show previews* to *always* in the *Notification* preference pane. + +> NOTE: The number of permissions required increases with each version of macOS, so if you are using an older OS, you may not be asked for one or more permissions. + +Then it asks your login password to unlock the lock screen. +It will be stored safely in Keychain. + +Finally, from the menu bar icon, select *Device*. +It starts scanning nearby BLE devices. +Select your device, and you're done! -On the first launch, it asks your login password, -which is required to unlock the lock screen. -It's safe because it's stored in Keychain. +## Options -Then it asks for permission for Accessibility. -In System Preferences unlock by clicking lock icon and turn BLEUnlock on. -It is also required to unlock the lock screen. +Option | Description +-------|--- +Lock Screen Now | It locks the screen regardless of whether the BLE device is nearby or not; it will unlock once the BLE device moves away and then moves closer again. This is useful to ensure that the screen is locked before you leave your seat. +Unlock RSSI | Bluetooth signal strength to unlock. Larger value indicates that the BLE device needs to be closer to the Mac to unlock. Choose *Disable* to disable unlocking. +Lock RSSI | Bluetooth signal strength to lock. Smaller value indicates that the BLE device needs to be farther away from the Mac to lock. Choose *Disable* to disable locking. +Delay to Lock | Duration of time before it locks the Mac when it detects that the BLE device is away. If the BLE device comes closer within that time, no lock will occur. +No-Signal Timeout | Time between last signal reception and locking. If you experience frequent "Signal is lost" locking, increase this value. +Wake on Proximity | Wakes up the display from sleep when the BLE device approaches while locking. +Wake without Unlocking | BLEUnlock will not unlock the Mac when the display wakes up from sleep, whether automatically via "Wake on Proximity" or manually. This allows for compatibility with the macOS built-in unlock with Apple Watch feature (which can operate immediately after BLEUnlock wakes the screen), or if you just prefer the lock screen to appear more quickly but don't want it to auto-unlock. +Pause "Now Playing" while Locked | On lock/unlock, BLEUnlock pauses/unpauses playback of music or video (including Apple Music, QuickTime Player and Spotify) that is controlled by *Now Playing* widget or the ⏯ key on the keyboard. +Use Screensaver to Lock | If this option is set, BLEUnlock launches screensaver instead of locking. For this option to work properly, you need to set *Require password **immediately** after sleep or screen saver begins* option in *Security & Privacy* preference pane. +Turn Off Screen on Lock | Turn off the display immediately when locking. +Set Password... | If you changed your login password, use this. +Passive Mode | By default it actively tries to connect to the BLE device and read the RSSI. Most of the time, the default is recommended and works stably. However, if you are using other Bluetooth things like keyboard, mouse, track pad or most notably Bluetooth Personal Hotspot, the default mode may interfere with each other. 2.4GHz WiFi may interfere as well. If you are experiencing instability of Bluetooth, turn on Passive Mode. +Launch at Login | Launches BLEUnlock when you login. +Set Minimum RSSI | Devices with RSSI below this value will not be displayed in the device scan list. ## Troubleshooting -If it fails to unlock, check BLEUnlock is turned on in "System Preferences" → "Security & Privacy" → "Privacy" → "Accessibility". +### Can't find my device in the list + +If your BLE device is not from Apple, BLEUnlock may not able to find the device name. +If that is the case, your device is displayed as a UUID (long hexadecimal numbers and hyphens). +To identify the device, try moving the device closer to or farther away from the Mac and see if the RSSI (dB value) changes accordingly. + +If you don't see *any* device in the list, try resetting the Bluetooth module as described below. + +### It fails to unlock + +Make sure BLEUnlock is turned on in *System Preferences* > *Security & Privacy* > *Privacy* > *Accessibility*. If it is already on, try turning it off and on again. -If it asks for permission to access its own password, click "Always Allow", because it is needed while the screen is locked. +If it asks for permission to access its own password in Keychain, you must choose *Always Allow*, because it is needed while the screen is locked. + +### "Signal is lost" occurs frequently + +Increase *No-Signal Timeout*. +Or try *Passive Mode*. + +### My Bluetooth keyboard, mouse, Personal Hotspot, or whatever Bluetooth, went nuts! + +Firstly, Shift + Option + Click the Bluetooth icon in the menubar or Control Center, then click *Reset the Bluetooth module*. + +In macOS 12 Monterey, this option is no longer available. +Instead, type the command below in Terminal to reset the Bluetooth module: + +``` +sudo pkill bluetoothd +``` + +This command will ask your login password. + +If the problem persists, turn on *Passive Mode*. + +## Notes on MAC address + +Unlike classic Bluetooth, Bluetooth Low Energy devices can use *private* MAC address. +That private address can be random, and can be changed from time to time. + +Recent smart devices, both iOS and Android, tend to use private addresses that change every 15 minutes or so. This is probably to prevent tracking. + +On the other hand, in order for BLEUnlock to track your device, its MAC address must be static. + +Fortunately, on Apple devices, if you are signed in with the same Apple ID as your Mac, the MAC address is resolved to the true (public) address. + +For other devices, including Android, the way to resolve the address is unknown. +If your non-Apple device changes its MAC address over time, unfortunately BLEUnlock can't support it. + +To check if the MAC address is resolved correctly, compare the MAC address displayed in the *Device* scan list of BLEUnlock with the one that is displayed on your device. + +## Run script on lock/unlock + +On locking and unlocking, BLEUnlock runs a script located here: + +``` +~/Library/Application Scripts/jp.sone.BLEUnlock/event +``` + +An argument is passed depending on the type of event: + +|Event|Argument| +|-----|--------| +|Locked by BLEUnlock because of low RSSI|`away`| +|Locked by BLEUnlock because of no signal|`lost`| +|Unlocked by BLEUnlock|`unlocked`| +|Unlocked manually|`intruded`| + +> NOTE: for `intruded` event works properly, you have to set *Require password **immediately** after sleep* in *Security & Privacy* preference pane. + +### Example + +Here is an example script which sends a LINE Notify message, with a photo of the person in front of the Mac when it is unlocked manually. + +```sh +#!/bin/bash + +set -eo pipefail + +LINE_TOKEN=xxxxx + +notify() { + local message=$1 + local image=$2 + if [ "$image" ]; then + img_arg="-F imageFile=@$image" + else + img_arg="" + fi + curl -X POST -H "Authorization: Bearer $LINE_TOKEN" -F "message=$message" \ + $img_arg https://notify-api.line.me/api/notify +} + +capture() { + open -Wa SnapshotUnlocker + ls -t /tmp/unlock-*.jpg | head -1 +} + +case $1 in + away) + notify "$(hostname -s) is locked by BLEUnlock because iPhone is away." + ;; + lost) + notify "$(hostname -s) is locked by BLEUnlock because signal is lost." + ;; + unlocked) + #notify "$(hostname -s) is unlocked by BLEUnlock." + ;; + intruded) + notify "$(hostname -s) is manually unlocked." $(capture) + ;; +esac +``` + +`SnapshotUnlocker` is an .app created with Script Editor with this script: + +``` +do shell script "/usr/local/bin/ffmpeg -f avfoundation -r 30 -i 0 -frames:v 1 -y /tmp/unlock-$(date +%Y%m%d_%H%M%S).jpg" +``` + +This app is required because BLEUnlock does not have Camera permission. +Giving permission to this app resolves the problem. + +## Funding + +The annual Apple Developer Program fee is funded by donations. + +If you like this app, I'd appreciate it if you could make a donation via [Buy Me a Coffee](https://www.buymeacoffee.com/tsone) or [PayPal Me](https://www.paypal.com/paypalme/my/profile) so I can keep up. + +## Credits + +- [peiit](https://github.com/peiit): Chinese translation +- [wenmin-wu](https://github.com/wenmin-wu): Minimum RSSI and moving average +- [stephengroat](https://github.com/stephengroat): CI +- [joeyhoer](https://github.com/joeyhoer): Homebrew Cask +- [Skyearn](https://github.com/Skyearn): Big Sur style icon +- [cyberclaus](https://github.com/cyberclaus): German, Swedish, Norwegian (Bokmål) and Danish localizations +- [alonewolfx2](https://github.com/alonewolfx2): Turkish localization +- [wernjie](https://github.com/wernjie): Wake without Unlocking +- [tokfrans03](https://github.com/tokfrans03): Language fixes -## Acknowledgement -Icon is based on `lock-open.svg` downloaded from materialdesignicons.com. +Icons are based on SVGs downloaded from materialdesignicons.com. +They are originally designed by Google LLC and licensed under Apache License version 2.0. ## License MIT -Copyright © 2019 by Takeshi Sone. +Copyright © 2019-2022 Takeshi Sone. diff --git a/Tests/GatewayParsingTests/main.swift b/Tests/GatewayParsingTests/main.swift new file mode 100644 index 00000000..be52f91a --- /dev/null +++ b/Tests/GatewayParsingTests/main.swift @@ -0,0 +1,39 @@ +import Foundation + +var failures = 0 +func check(_ cond: Bool, _ msg: String) { + if cond { print("ok - \(msg)") } else { print("FAIL - \(msg)"); failures += 1 } +} + +let routeOut = """ + route to: default +destination: default + mask: default + gateway: 10.0.0.1 + interface: en0 + flags: +""" +check(parseDefaultGatewayIP(routeOutput: routeOut) == "10.0.0.1", "parse gateway IP from route output") +check(parseDefaultGatewayIP(routeOutput: "no default route\n") == nil, "no gateway line -> nil") + +// macOS `arp` prints octets WITHOUT leading zeros; normalise so allowlist matches. +check(normalizeMAC("a4:2b:8c:1:2:3") == "a4:2b:8c:01:02:03", "pad single-digit octets") +check(normalizeMAC("A4:2B:8C:01:02:03") == "a4:2b:8c:01:02:03", "lower-case octets") +check(normalizeMAC("a4:2b:8c:1:2") == nil, "five octets -> nil") +check(normalizeMAC("zz:2b:8c:1:2:3") == nil, "non-hex octet -> nil") +// Hardening: Swift's Int(_:radix:) accepts a leading sign, so without a hex-alphabet +// guard "-1"/"+a" would slip through and yield a malformed (non-nil) MAC. +check(normalizeMAC("-1:2b:8c:1:2:3") == nil, "sign-prefixed octet -> nil") +check(normalizeMAC("+a:2b:8c:1:2:3") == nil, "plus-prefixed octet -> nil") + +let arpOut = "? (10.0.0.1) at a4:2b:8c:1:2:3 on en0 ifscope [ethernet]" +check(parseGatewayMAC(arpOutput: arpOut) == "a4:2b:8c:01:02:03", "parse + normalise MAC from arp output") +check(parseGatewayMAC(arpOutput: "? (10.0.0.1) at (incomplete) on en0 ifscope [ethernet]") == nil, "incomplete arp entry -> nil") + +let allow: Set = ["a4:2b:8c:01:02:03"] +check(computePauseState(resolvedMAC: "a4:2b:8c:01:02:03", allowlist: allow, previous: false) == true, "MAC in allowlist -> paused") +check(computePauseState(resolvedMAC: "ff:ff:ff:ff:ff:ff", allowlist: allow, previous: true) == false, "MAC not in allowlist -> active") +check(computePauseState(resolvedMAC: nil, allowlist: allow, previous: true) == true, "nil keeps previous state (sticky true)") +check(computePauseState(resolvedMAC: nil, allowlist: allow, previous: false) == false, "nil keeps previous state (sticky false)") + +if failures == 0 { print("\nALL PASS") } else { print("\n\(failures) FAILURE(S)"); exit(1) } diff --git a/apple-device-names b/apple-device-names new file mode 100755 index 00000000..c2d1db9a --- /dev/null +++ b/apple-device-names @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import re, json +from glob import glob +from subprocess import Popen, PIPE + +mapping = {} + +def parse_info(filename): + p = Popen([ + 'plutil', + '-convert', 'json', + f, + '-o', '-' + ], stdout=PIPE) + + data = json.load(p.stdout) + + for item in data['UTExportedTypeDeclarations']: + if 'UTTypeTagSpecification' not in item: continue + for code in item['UTTypeTagSpecification']['com.apple.device-model-code']: + if re.match(r'^[a-zA-Z]+\d+,\d+$', code): + name = item['UTTypeDescription'] + name = re.sub(r'Model A?\d+(, A?\d+)*', '', name) + name = re.sub(r' *?\)', ')', name) + name = re.sub(r' \(\)', '', name) + mapping[code] = name + +for f in glob('/System/Library/CoreServices/CoreTypes.bundle/Contents/Library/MobileDevices*.bundle/Contents/Info.plist'): + parse_info(f) + +print('let appleDeviceNames = [') +for code, name in mapping.items(): + print(f' "{code}": "{name}",') +print(']') diff --git a/icons/.gitignore b/icons/.gitignore index e33609d2..66335542 100644 --- a/icons/.gitignore +++ b/icons/.gitignore @@ -1 +1,2 @@ *.png +*.pdf diff --git a/icons/mkicons b/icons/mkicons index dfb06cfe..ed1738ac 100755 --- a/icons/mkicons +++ b/icons/mkicons @@ -3,3 +3,6 @@ for s in 16 32 64 128 256 512 1024; do svg2png -w $s -h $s unlock-with-circle.svg app-$s.png done svg2png -w 1280 -h 800 unlock-with-circle.svg app-preview.png + +svg2pdf -w 18 -h -18 unlock.svg unlock-on.pdf +svg2pdf -w 18 -h -18 unlock-outline.svg unlock-off.pdf diff --git a/icons/unlock-outline.svg b/icons/unlock-outline.svg new file mode 100644 index 00000000..2be8c819 --- /dev/null +++ b/icons/unlock-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/buymeacoffee.svg b/img/buymeacoffee.svg new file mode 100644 index 00000000..d2d50b01 --- /dev/null +++ b/img/buymeacoffee.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/release b/release index 33b31d33..56415519 100755 --- a/release +++ b/release @@ -1,9 +1,14 @@ -#!/bin/bash -xe +#!/bin/bash -e set -o pipefail -USERNAME=apple@bounce-inc.jp -TEAM=376NR42P9A +if [ ! "$PASSWORD" ]; then + echo set app-specific password in PASSWORD env var + exit 1 +fi + +USERNAME=takeshi.sone@gmail.com +TEAM=42LGPQYC7M APPNAME=BLEUnlock BASEDIR=$(cd $(dirname $0); pwd) @@ -25,21 +30,20 @@ notarize() { rm -f $TMPDIR/upload.zip ditto -c -k --keepParent $app $TMPDIR/upload.zip - xcrun altool --notarize-app --primary-bundle-id $id --username $USERNAME \ - --password "@keychain:AC_PASSWORD" --file $TMPDIR/upload.zip \ - --asc-provider $TEAM 2>&1 | tee $TMPDIR/altool.log - uuid=$(awk '/^RequestUUID/ { print $3 }' $TMPDIR/altool.log) + xcrun notarytool submit --apple-id $USERNAME --password "$PASSWORD" $TMPDIR/upload.zip \ + --team-id $TEAM 2>&1 | tee $TMPDIR/altool.log + uuid=$(awk '/ id:/ { print $2; exit }' $TMPDIR/altool.log) while true; do - sleep 10 - xcrun altool --notarization-info $uuid --username $USERNAME \ - --password "@keychain:AC_PASSWORD" --asc-provider 376NR42P9A 2>&1 | - tee $TMPDIR/altool.log - status=$(grep Status: $TMPDIR/altool.log | sed 's/^.*Status: *//') - if [ "$status" != "in progress" ]; then + sleep 2 + xcrun notarytool info $uuid --apple-id $USERNAME \ + --password "$PASSWORD" --team-id $TEAM 2>&1 | + tee $TMPDIR/altool.log || true + status=$(grep status: $TMPDIR/altool.log | sed 's/^.*status: *//') + if [ "$status" ] && [ "$status" != "In Progress" ]; then break fi done - if [ "$status" != "success" ]; then + if [ "$status" != "Accepted" ]; then exit 1 fi xcrun stapler staple $app @@ -66,3 +70,7 @@ notarize $RELEASEDIR/$APPNAME.app version=$(extract_info $RELEASEDIR/$APPNAME.app CFBundleShortVersionString) ditto -c -k --keepParent $RELEASEDIR/$APPNAME.app $RELEASEDIR/$APPNAME-$version.zip + +v=$version-$(extract_info $RELEASEDIR/$APPNAME.app CFBundleVersion) +mkdir -p archives/$v +cp -a "$RELEASEDIR/$APPNAME.xcarchive" archives/$v