From c2e5259cff0322636b67589412d90f22bb250f2c Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Mon, 2 Jan 2017 12:24:41 -0500 Subject: [PATCH] Notify user on low power situations (#19) --- Juice.xcodeproj/project.pbxproj | 39 +++++++--- Juice/Classes/LowPowerCoordinator.swift | 76 ++++++++++++++++++++ Juice/Classes/StatusBarItemCoordinator.swift | 5 ++ Juice/Info.plist | 4 +- JuiceHelper/Info.plist | 2 +- en.lproj/Localizable.strings | 2 + 6 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 Juice/Classes/LowPowerCoordinator.swift diff --git a/Juice.xcodeproj/project.pbxproj b/Juice.xcodeproj/project.pbxproj index c8eb370..828bf57 100644 --- a/Juice.xcodeproj/project.pbxproj +++ b/Juice.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 725AA5E01E05DBDB006E5DDE /* PreferencesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 725AA5DF1E05DBDB006E5DDE /* PreferencesCoordinator.swift */; }; 725AA5E51E05DE22006E5DDE /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 725AA5E31E05DE22006E5DDE /* PreferencesWindowController.swift */; }; 725AA5E61E05DE22006E5DDE /* PreferencesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 725AA5E41E05DE22006E5DDE /* PreferencesWindowController.xib */; }; + 728C64581E1AB52F008DA4A6 /* LowPowerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 728C64571E1AB52F008DA4A6 /* LowPowerCoordinator.swift */; }; 728EB92E1E0601D800920B83 /* ChargeScaleDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 728EB92D1E0601D800920B83 /* ChargeScaleDisplay.swift */; }; 728EB9311E06033300920B83 /* PowerSource+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 728EB9301E06033300920B83 /* PowerSource+Extensions.swift */; }; 728EB9341E0605CB00920B83 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 728EB9331E0605CB00920B83 /* FileManager+Extensions.swift */; }; @@ -73,6 +74,7 @@ 725AA5DF1E05DBDB006E5DDE /* PreferencesCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesCoordinator.swift; path = Classes/PreferencesCoordinator.swift; sourceTree = ""; }; 725AA5E31E05DE22006E5DDE /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesWindowController.swift; path = Classes/PreferencesWindowController.swift; sourceTree = ""; }; 725AA5E41E05DE22006E5DDE /* PreferencesWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PreferencesWindowController.xib; path = Classes/PreferencesWindowController.xib; sourceTree = ""; }; + 728C64571E1AB52F008DA4A6 /* LowPowerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LowPowerCoordinator.swift; path = Classes/LowPowerCoordinator.swift; sourceTree = ""; }; 728EB92D1E0601D800920B83 /* ChargeScaleDisplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ChargeScaleDisplay.swift; path = Classes/ChargeScaleDisplay.swift; sourceTree = ""; }; 728EB9301E06033300920B83 /* PowerSource+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "PowerSource+Extensions.swift"; path = "Classes/PowerSource+Extensions.swift"; sourceTree = ""; }; 728EB9321E0604D400920B83 /* Juice.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Juice.entitlements; sourceTree = ""; }; @@ -138,7 +140,6 @@ 725AA5B31E05BF3A006E5DDE = { isa = PBXGroup; children = ( - AAA3A2A51E0F3F0F0015165E /* Localizable.strings */, 72DA73671E09FEA7003F9262 /* Shared */, 725AA5BE1E05BF3A006E5DDE /* Juice */, 72DA735B1E09FE19003F9262 /* JuiceHelper */, @@ -160,16 +161,14 @@ 725AA5BE1E05BF3A006E5DDE /* Juice */ = { isa = PBXGroup; children = ( - 728EB9321E0604D400920B83 /* Juice.entitlements */, + 728C64591E1ABA35008DA4A6 /* Resources */, 728EB92F1E06032600920B83 /* Extensions */, 728EB92C1E0601A700920B83 /* Display String Parsing */, + 728C64561E1AB51D008DA4A6 /* Low Power */, 725AA5DE1E05DBCD006E5DDE /* Preferences */, 725AA5D11E05C18A006E5DDE /* Main Application Hierarchy */, 725AA5CC1E05C083006E5DDE /* Power Source */, 725AA5BF1E05BF3A006E5DDE /* AppDelegate.swift */, - 725AA5C11E05BF3A006E5DDE /* Assets.xcassets */, - 725AA5C31E05BF3A006E5DDE /* MainMenu.xib */, - 725AA5C61E05BF3A006E5DDE /* Info.plist */, ); path = Juice; sourceTree = ""; @@ -215,6 +214,26 @@ name = Preferences; sourceTree = ""; }; + 728C64561E1AB51D008DA4A6 /* Low Power */ = { + isa = PBXGroup; + children = ( + 728C64571E1AB52F008DA4A6 /* LowPowerCoordinator.swift */, + ); + name = "Low Power"; + sourceTree = ""; + }; + 728C64591E1ABA35008DA4A6 /* Resources */ = { + isa = PBXGroup; + children = ( + 725AA5C61E05BF3A006E5DDE /* Info.plist */, + 725AA5C31E05BF3A006E5DDE /* MainMenu.xib */, + 725AA5C11E05BF3A006E5DDE /* Assets.xcassets */, + AAA3A2A51E0F3F0F0015165E /* Localizable.strings */, + 728EB9321E0604D400920B83 /* Juice.entitlements */, + ); + name = Resources; + sourceTree = ""; + }; 728EB92C1E0601A700920B83 /* Display String Parsing */ = { isa = PBXGroup; children = ( @@ -462,6 +481,7 @@ 725AA5C01E05BF3A006E5DDE /* AppDelegate.swift in Sources */, 725AA5D31E05C19A006E5DDE /* ApplicationCoordinator.swift in Sources */, 725AA5E01E05DBDB006E5DDE /* PreferencesCoordinator.swift in Sources */, + 728C64581E1AB52F008DA4A6 /* LowPowerCoordinator.swift in Sources */, 725AA5E51E05DE22006E5DDE /* PreferencesWindowController.swift in Sources */, 728EB93A1E061B4700920B83 /* PreferencesStorage.swift in Sources */, 725AA5DD1E05D3CD006E5DDE /* StatusItemMenu.swift in Sources */, @@ -510,6 +530,7 @@ AAA3A2A71E0F3F150015165E /* fr */, ); name = Localizable.strings; + path = ..; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -617,7 +638,7 @@ CODE_SIGN_ENTITLEMENTS = Juice/Juice.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 529; + CURRENT_PROJECT_VERSION = 2017.01.02105829; DEVELOPMENT_TEAM = YN24FFRTC8; INFOPLIST_FILE = Juice/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; @@ -635,7 +656,7 @@ CODE_SIGN_ENTITLEMENTS = Juice/Juice.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 529; + CURRENT_PROJECT_VERSION = 2017.01.02105829; DEVELOPMENT_TEAM = YN24FFRTC8; INFOPLIST_FILE = Juice/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; @@ -652,7 +673,7 @@ CODE_SIGN_ENTITLEMENTS = JuiceHelper/JuiceHelper.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 529; + CURRENT_PROJECT_VERSION = 2017.01.02105829; DEVELOPMENT_TEAM = YN24FFRTC8; INFOPLIST_FILE = JuiceHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; @@ -670,7 +691,7 @@ CODE_SIGN_ENTITLEMENTS = JuiceHelper/JuiceHelper.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 529; + CURRENT_PROJECT_VERSION = 2017.01.02105829; DEVELOPMENT_TEAM = YN24FFRTC8; INFOPLIST_FILE = JuiceHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; diff --git a/Juice/Classes/LowPowerCoordinator.swift b/Juice/Classes/LowPowerCoordinator.swift new file mode 100644 index 0000000..abc7f3a --- /dev/null +++ b/Juice/Classes/LowPowerCoordinator.swift @@ -0,0 +1,76 @@ +// +// LowPowerCoordinator.swift +// Juice +// +// Created by Brian Michel on 1/2/17. +// Copyright © 2017 Brian Michel. All rights reserved. +// + +import AppKit +import RxSwift + +final class LowPowerCoordinator { + private enum Constants { + //TODO: Could probably make this a user preference + static let lowPowerNotificationPercentage = 10 + static let lowPowerNotificationIdentifier = "com.bsm.macos.juice.low-power-notification" + } + private let powerSourceObservable: Observable<[PowerSource]> + private let notificationCenter = NSUserNotificationCenter.default + private let disposeBag = DisposeBag() + private var lastPowerState: PowerSourceState = .unknown + + private var notificationHasBeenDelivered: Bool { + return notificationCenter.deliveredNotifications.filter({ $0.identifier == Constants.lowPowerNotificationIdentifier }).count > 0 + } + + init(observable: Observable<[PowerSource]>) { + powerSourceObservable = observable + } + + func start() { + powerSourceObservable.shareReplay(1).subscribe { (event) in + switch event { + case .next(let sources): + guard let source = sources.first else { + return + } + self.checkForNotificationDismissal(powerSource: source) + self.checkForWarning(powerSource: source) + case .error(let error): + print("Error: \(error)") + case .completed: + break + } + }.addDisposableTo(disposeBag) + } + + private func checkForNotificationDismissal(powerSource: PowerSource) { + if lastPowerState == .battery + && powerSource.state == .ac { + clearDeliveredNotification() + } + + lastPowerState = powerSource.state + } + + private func checkForWarning(powerSource: PowerSource) { + if powerSource.chargedPercentage <= Constants.lowPowerNotificationPercentage + && powerSource.state == .battery + && !notificationHasBeenDelivered { + let userNotification = NSUserNotification() + userNotification.title = NSLocalizedString("Low Power", comment: "Title of notification shown to user when their battery is very low.") + userNotification.informativeText = NSLocalizedString("Your Mac will sleep soon unless plugged into a power outlet.", comment: "Body of notification shown to user when their battery is very low.") + userNotification.hasActionButton = false + userNotification.identifier = Constants.lowPowerNotificationIdentifier + + notificationCenter.deliver(userNotification) + } + } + + private func clearDeliveredNotification() { + let userNotification = NSUserNotification() + userNotification.identifier = Constants.lowPowerNotificationIdentifier + notificationCenter.removeDeliveredNotification(userNotification) + } +} diff --git a/Juice/Classes/StatusBarItemCoordinator.swift b/Juice/Classes/StatusBarItemCoordinator.swift index 3688ad1..c3e734a 100644 --- a/Juice/Classes/StatusBarItemCoordinator.swift +++ b/Juice/Classes/StatusBarItemCoordinator.swift @@ -11,6 +11,7 @@ import RxSwift final class StatusBarItemCoordinator: StatusMenuItemDelegate { private let preferencesCoordinator = PreferencesCoordinator() + private let lowPowerCoordinator: LowPowerCoordinator private let statusBarItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) private let statusBarItemMenu = StatusMenuItem(title: "Juice") @@ -35,6 +36,8 @@ final class StatusBarItemCoordinator: StatusMenuItemDelegate { return disposable }) + + lowPowerCoordinator = LowPowerCoordinator(observable: powerSourcesObservable) scaleChangeObservable = preferencesStorage .chargeDisplayScale @@ -56,6 +59,8 @@ final class StatusBarItemCoordinator: StatusMenuItemDelegate { scaleChangeObservable.subscribe(onNext: { (scale) in self.updateLabel(scale: scale) }).addDisposableTo(disposableBag) + + lowPowerCoordinator.start() } private func updateLabel(source: PowerSource) { diff --git a/Juice/Info.plist b/Juice/Info.plist index 175dae0..1858333 100644 --- a/Juice/Info.plist +++ b/Juice/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 529 + 2017.01.02105829 LSApplicationCategoryType public.app-category.lifestyle LSMinimumSystemVersion @@ -32,5 +32,7 @@ MainMenu NSPrincipalClass NSApplication + NSUserNotificationAlertStyle + alert diff --git a/JuiceHelper/Info.plist b/JuiceHelper/Info.plist index c4beafe..e14228d 100644 --- a/JuiceHelper/Info.plist +++ b/JuiceHelper/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 529 + 2017.01.02105829 LSBackgroundOnly LSMinimumSystemVersion diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 8393e09..03c7d88 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -31,3 +31,5 @@ "Unknown Build"="Unknown Build"; "Credits for this app"="Credits for this app"; "Indicates the current battery charge percentage"="Indicates the current battery charge percentage"; +"Low Power"="Low Power"; +"Your Mac will sleep soon unless plugged into a power outlet."="Your Mac will sleep soon unless plugged into a power outlet.";