Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 137 additions & 97 deletions HowOnline/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,100 +7,140 @@
//

import Cocoa
import ReachabilitySwift
import StartAtLoginController

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, ProberDelegate {
@IBOutlet weak var statusMenuItem: NSMenuItem!
@IBOutlet weak var menu: NSMenu!
@IBOutlet weak var startAtLoginController: StartAtLoginController!

let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSSquareStatusItemLength)
var refreshTimer: NSTimer! = nil
var reachability: Reachability?
var prober: Prober!

func applicationDidFinishLaunching(aNotification: NSNotification) {
// The menu is defined in interface builder
statusItem.menu = self.menu
updateMenu(false, text: "hello", longText: "Hello")

startReachability()

refreshTimer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: #selector(probe), userInfo: nil, repeats: true)
prober = Prober(delegate: self)
probe()
}

func startReachability() {
let refreshBlock: (Reachability) -> () = { reachability in
dispatch_async(dispatch_get_main_queue()) {
self.probe()
}
}

do {
// We dont need any more sophistication than just checking for a wifi connection
// since we do our own internet connection checking
let reachability = try Reachability.reachabilityForLocalWiFi()
self.reachability = reachability

reachability.whenReachable = refreshBlock
reachability.whenUnreachable = refreshBlock
try reachability.startNotifier()
} catch {
print("Unable to create Reachability")
return
}
}

func probeResult(prober: Prober, result: Prober.ProbeResult) {
updateMenu(result.success, text: result.text, longText: result.longText)
}

func updateMenu(success: Bool, text: String, longText: String) {
if let button = statusItem.button {
button.image = imageForStatus(text, filledBar: success, imageSize: button.frame.size)
statusMenuItem.title = "Status: \(longText)"

// Make the icon black and white, but this will also auto reverse colors in Dark/highlighted mode
button.image!.template = true
}
}

func probe() {
prober.probe()
}

func imageForStatus(text: String, filledBar: Bool, imageSize: NSSize) -> NSImage? {
return NSImage(size: imageSize, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in
NSColor.blackColor().setFill()
NSColor.blackColor().setStroke()

let progressBarRect = CGRect(x: 2, y: rect.size.height - 4 - 4, width: rect.size.width - 2 - 2, height: 4)
let path = NSBezierPath(roundedRect: progressBarRect, xRadius: 2, yRadius: 2)

path.stroke()
if filledBar {
path.fill()
}

let textRect = CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height - 9)

// The text needs to be really small to fit
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .Center
paragraphStyle.lineBreakMode = .ByClipping

let attrs = [
NSFontAttributeName: NSFont.systemFontOfSize(8),
NSParagraphStyleAttributeName: paragraphStyle
]

text.drawWithRect(textRect, options: .UsesLineFragmentOrigin, attributes: attrs, context: nil)

return true
})
}
}
import CoreLocation

@main
class AppDelegate: NSObject, NSApplicationDelegate, ProberDelegate, CLLocationManagerDelegate {
@IBOutlet weak var statusMenuItem: NSMenuItem!
@IBOutlet weak var menu: NSMenu!

let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
var refreshTimer: Timer?
var prober: Prober!
var locationManager: CLLocationManager!
var locationAuthorized = false

func applicationDidFinishLaunching(_ aNotification: Notification) {
// The menu is defined in interface builder
statusItem.menu = self.menu
updateMenu(success: false, text: "hello", longText: "Hello")

// Setup location manager for SSID access (required since macOS 10.15)
setupLocationServices()

prober = Prober(delegate: self)

// Start probing after a short delay to allow location auth
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.startProbing()
}
}

func setupLocationServices() {
locationManager = CLLocationManager()
locationManager.delegate = self

// Check current authorization status
let status: CLAuthorizationStatus
if #available(macOS 11.0, *) {
status = locationManager.authorizationStatus
} else {
status = CLLocationManager.authorizationStatus()
}

switch status {
case .authorizedAlways:
locationAuthorized = true
case .notDetermined:
// Request authorization - on macOS this uses "When In Use" style
if #available(macOS 10.15, *) {
locationManager.requestWhenInUseAuthorization()
}
case .denied, .restricted:
print("Location access denied - SSID detection may not work")
locationAuthorized = false
@unknown default:
locationAuthorized = false
}
}

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status: CLAuthorizationStatus
if #available(macOS 11.0, *) {
status = manager.authorizationStatus
} else {
status = CLLocationManager.authorizationStatus()
}

locationAuthorized = (status == .authorizedAlways)

if locationAuthorized {
probe()
}
}

// Legacy delegate method for older macOS
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationAuthorized = (status == .authorizedAlways)

if locationAuthorized {
probe()
}
}

func startProbing() {
refreshTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
self?.probe()
}
probe()
}

func probeResult(_ prober: Prober, result: Prober.ProbeResult) {
updateMenu(success: result.success, text: result.text, longText: result.longText)
}

func updateMenu(success: Bool, text: String, longText: String) {
if let button = statusItem.button {
button.image = imageForStatus(text: text, filledBar: success, imageSize: button.frame.size)
statusMenuItem.title = "Status: \(longText)"

// Make the icon black and white, but this will also auto reverse colors in Dark/highlighted mode
button.image?.isTemplate = true
}
}

@objc func probe() {
prober.probe()
}

func imageForStatus(text: String, filledBar: Bool, imageSize: NSSize) -> NSImage? {
return NSImage(size: imageSize, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in
NSColor.black.setFill()
NSColor.black.setStroke()

let progressBarRect = CGRect(x: 2, y: rect.size.height - 4 - 4, width: rect.size.width - 2 - 2, height: 4)
let path = NSBezierPath(roundedRect: progressBarRect, xRadius: 2, yRadius: 2)

path.stroke()
if filledBar {
path.fill()
}

let textRect = CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height - 9)

// The text needs to be really small to fit
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byClipping

let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 8),
.paragraphStyle: paragraphStyle
]

text.draw(with: textRect, options: .usesLineFragmentOrigin, attributes: attrs, context: nil)

return true
})
}
}
2 changes: 2 additions & 0 deletions HowOnline/HowOnline.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>
8 changes: 6 additions & 2 deletions HowOnline/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.01</string>
<string>1.1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>4</string>
<string>5</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
Expand All @@ -32,5 +32,9 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>HowOnline needs location access to detect your WiFi network name (SSID). This is required by macOS 10.15+ for accessing WiFi information.</string>
<key>NSLocationUsageDescription</key>
<string>HowOnline needs location access to detect your WiFi network name (SSID). This is required by macOS 10.15+ for accessing WiFi information.</string>
</dict>
</plist>
109 changes: 54 additions & 55 deletions HowOnline/Pinger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,58 @@ import Foundation
import QuartzCore

class Pinger: NSObject, SimplePingDelegate {
typealias CompletionBlock = (timeElapsedMs: Int?) -> ()
var completionBlock: CompletionBlock!
typealias CompletionBlock = (Int?) -> Void
var completionBlock: CompletionBlock?

var pinger: SimplePing!
var timeoutTimer: NSTimer!
var pingStartTime: CFTimeInterval = 0

func ping(hostname: String, completionBlock: CompletionBlock) {
self.pinger = SimplePing(hostName: hostname)
self.completionBlock = completionBlock

pingStartTime = CACurrentMediaTime()

pinger.delegate = self;
pinger.start()
}

func stopPinging(success: Bool) {
pinger.stop()
if timeoutTimer != nil {
timeoutTimer.invalidate()
}

if success {
let elapsedTime = Int((CACurrentMediaTime() - pingStartTime) * 1000)
self.completionBlock(timeElapsedMs: elapsedTime)
} else {
self.completionBlock(timeElapsedMs: nil)
}
}

func timeout() {
stopPinging(false)
}

func simplePing(pinger: SimplePing!, didFailToSendPacket packet: NSData!, error: NSError!) {
stopPinging(false)
}

func simplePing(pinger: SimplePing!, didFailWithError error: NSError!) {
stopPinging(false)
}

func simplePing(pinger: SimplePing!, didReceivePingResponsePacket packet: NSData!) {
stopPinging(true)
}

func simplePing(pinger: SimplePing!, didReceiveUnexpectedPacket packet: NSData!) {
stopPinging(false)
}

func simplePing(pinger: SimplePing!, didStartWithAddress address: NSData!) {
timeoutTimer = NSTimer.scheduledTimerWithTimeInterval(2.5, target: self, selector: #selector(timeout), userInfo: nil, repeats: false)
pinger.sendPingWithData(nil)
}
}
var pinger: SimplePing?
var timeoutTimer: Timer?
var pingStartTime: CFTimeInterval = 0

func ping(hostname: String, completionBlock: @escaping CompletionBlock) {
self.pinger = SimplePing(hostName: hostname)
self.completionBlock = completionBlock

pingStartTime = CACurrentMediaTime()

pinger?.delegate = self
pinger?.start()
}

func stopPinging(success: Bool) {
pinger?.stop()
timeoutTimer?.invalidate()
timeoutTimer = nil

if success {
let elapsedTime = Int((CACurrentMediaTime() - pingStartTime) * 1000)
self.completionBlock?(elapsedTime)
} else {
self.completionBlock?(nil)
}
}

@objc func timeout() {
stopPinging(success: false)
}

func simplePing(_ pinger: SimplePing, didFailToSendPacket packet: Data, error: Error) {
stopPinging(success: false)
}

func simplePing(_ pinger: SimplePing, didFailWithError error: Error) {
stopPinging(success: false)
}

func simplePing(_ pinger: SimplePing, didReceivePingResponsePacket packet: Data) {
stopPinging(success: true)
}

func simplePing(_ pinger: SimplePing, didReceiveUnexpectedPacket packet: Data) {
stopPinging(success: false)
}

func simplePing(_ pinger: SimplePing, didStartWithAddress address: Data) {
timeoutTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(timeout), userInfo: nil, repeats: false)
pinger.send(with: nil)
}
}
Loading