Skip to content

Commit

Permalink
Add power mode info
Browse files Browse the repository at this point in the history
  • Loading branch information
decodism committed Oct 11, 2023
1 parent c12c377 commit fc5d74c
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 7 deletions.
11 changes: 11 additions & 0 deletions BatFiKit/Sources/AppShared/PowerSettingInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Shared

public struct PowerSettingInfo {
public let powerMode: PowerMode
public let supportsHighPowerMode: Bool

public init(powerMode: PowerMode, supportsHighPowerMode: Bool) {
self.powerMode = powerMode
self.supportsHighPowerMode = supportsHighPowerMode
}
}
40 changes: 39 additions & 1 deletion BatFiKit/Sources/BatteryInfo/BatteryInfoView.Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import AsyncAlgorithms
import Clients
import Dependencies
import Foundation
import Shared

extension BatteryInfoView {
@MainActor
final class Model: ObservableObject {
@Dependency(\.powerSourceClient) private var powerSourceClient
@Dependency(\.appChargingState) private var appChargingState
@Dependency(\.defaults) private var defaults
@Dependency(\.powerSettingClient) private var powerSettingClient

private(set) var state: PowerState? {
didSet {
Expand All @@ -31,6 +33,24 @@ extension BatteryInfoView {
objectWillChange.send()
}
}

private(set) var powerSettingInfo: PowerSettingInfo? {
willSet {
objectWillChange.send()
}
}

var powerModeSelection: PowerMode? {
get { powerSettingInfo?.powerMode }
set {
// Resync picker selection
objectWillChange.send()
guard let mode = newValue else {
return
}
setPowerMode(mode)
}
}

private var tasks: [Task<Void, Never>]?

Expand All @@ -57,8 +77,14 @@ extension BatteryInfoView {
self.state = state
}
}

let powerSettingInfoChanges = Task {
for await info in powerSettingClient.powerSettingInfoChanges() {
self.powerSettingInfo = info
}
}

tasks = [powerSourceChanges, observeChargingStateMode]
tasks = [powerSourceChanges, observeChargingStateMode, powerSettingInfoChanges]
}

func cancelObserving() {
Expand All @@ -84,5 +110,17 @@ extension BatteryInfoView {
let measurement = Measurement(value: temperature, unit: UnitTemperature.celsius)
return temperatureFormatter.string(from: measurement)
}

private func setPowerMode(_ mode: PowerMode) {
guard
let sourceKey = state?.powerSource,
let source = PowerSource(key: sourceKey)
else {
return
}
Task {
try? await powerSettingClient.setPowerMode(mode, source)
}
}
}
}
29 changes: 23 additions & 6 deletions BatFiKit/Sources/BatteryInfo/BatteryInfoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI
import AppShared
import L10n
import Shared

public struct BatteryInfoView: View {
@StateObject private var model = Model()
Expand Down Expand Up @@ -56,6 +57,22 @@ public struct BatteryInfoView: View {
label: l10n.Additional.batteryCapacity,
info: percentageFormatter.string(from: NSNumber(floatLiteral: powerState.batteryCapacity))!
)
if let powerSettingInfo = model.powerSettingInfo {
BatteryAdditionalInfo(
label: { Text(l10n.Additional.powerMode) },
info: {
Picker(l10n.Additional.powerMode, selection: $model.powerModeSelection) {
Text(l10n.Additional.lowPowerMode).tag(PowerMode.low as PowerMode?)
Text(l10n.Additional.autoPowerMode).tag(PowerMode.auto as PowerMode?)
if powerSettingInfo.supportsHighPowerMode {
Text(l10n.Additional.highPowerMode).tag(PowerMode.high as PowerMode?)
}
}
.pickerStyle(.segmented)
.labelsHidden()
}
)
}
}
.frame(maxWidth: .infinity)
}
Expand Down Expand Up @@ -98,28 +115,28 @@ struct BatteryMainInfo: View {
}
}

struct BatteryAdditionalInfo<Label: View>: View {
struct BatteryAdditionalInfo<Label: View, Info: View>: View {
private let itemsSpace: CGFloat = 20

let label: () -> Label
let info: String
let info: () -> Info

init(label: @escaping () -> Label, info: String) {
init(label: @escaping () -> Label, info: @escaping () -> Info) {
self.label = label
self.info = info
}

init(label: String, info: String) where Label == Text {
init(label: String, info: String) where Label == Text, Info == Text {
self.label = { Text(label) }
self.info = info
self.info = { Text(info) }
}

var body: some View {
HStack {
Group {
label()
Spacer(minLength: itemsSpace)
Text(info)
info()
.multilineTextAlignment(.trailing)
}
.foregroundColor(.secondary)
Expand Down
25 changes: 25 additions & 0 deletions BatFiKit/Sources/Clients/PowerSettingClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import AppShared
import Dependencies
import Shared

public struct PowerSettingClient: TestDependencyKey {
public var powerSettingInfoChanges: () -> AsyncStream<PowerSettingInfo>
public var setPowerMode: (PowerMode, PowerSource) async throws -> Void

public init(
powerSettingInfoChanges: @escaping () -> AsyncStream<PowerSettingInfo>,
setPowerMode: @escaping (PowerMode, PowerSource) async throws -> Void
) {
self.powerSettingInfoChanges = powerSettingInfoChanges
self.setPowerMode = setPowerMode
}

public static var testValue: PowerSettingClient = unimplemented()
}

extension DependencyValues {
public var powerSettingClient: PowerSettingClient {
get { self[PowerSettingClient.self] }
set { self[PowerSettingClient.self] = newValue }
}
}
112 changes: 112 additions & 0 deletions BatFiKit/Sources/ClientsLive/PowerSettingClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import AppShared
import Clients
import Dependencies
import Foundation
import SecureXPC
import Shared
import SystemConfiguration

extension PowerSettingClient: DependencyKey {
public static var liveValue: PowerSettingClient {
func powerSettingInfo(_ store: SCDynamicStore) -> PowerSettingInfo? {
guard
let settings = SCDynamicStoreCopyValue(store, Private.kIOPMDynamicStoreSettingsKey as CFString) as? [String: Any],
let lowPowerMode = settings[Private.kIOPMLowPowerModeKey] as? UInt8,
let powerMode = PowerMode(rawValue: lowPowerMode)
else {
return nil
}
let supportsHighPowerMode = settings[Private.kIOPMHighPowerModeKey] != nil
return PowerSettingInfo(powerMode: powerMode, supportsHighPowerMode: supportsHighPowerMode)
}

func createClient() -> XPCClient {
XPCClient.forMachService(
named: Constant.helperBundleIdentifier,
withServerRequirement: try! .sameTeamIdentifier
)
}

let observer = Observer()
let client = Self(
powerSettingInfoChanges: {
AsyncStream { continuation in
guard let observer else {
return
}
if let info = powerSettingInfo(observer.store) {
continuation.yield(info)
}
let id = UUID()
observer.addHandler(id) {
if let info = powerSettingInfo(observer.store) {
continuation.yield(info)
}
}
continuation.onTermination = { _ in
observer.removeHandler(id)
}
}
},
setPowerMode: { powerMode, source in
let settings: [PowerSource: [PowerSetting]] = [source: [.powerMode(powerMode: powerMode)]]
let option = PowerSettingOption(settings: settings)
try await createClient().sendMessage(option, to: XPCRoute.powerSettingOption)
}
)
return client
}

private class Observer {
private let handlersQueue = DispatchQueue(label: "software.micropixels.BatFi.PowerSettingClient.Observer")

private var handlers = [UUID: () -> Void]()
private(set) var store: SCDynamicStore!

init?() {
guard let store = setUpObserving() else {
return nil
}
self.store = store
}

private func setUpObserving() -> SCDynamicStore? {
let info = Unmanaged.passUnretained(self).toOpaque()
var context = SCDynamicStoreContext()
context.info = info
guard
let store = SCDynamicStoreCreate(nil, "software.micropixels.BatFi" as CFString, { store, changedKeys, info in
guard let info else {
return
}
let observer = Unmanaged<Observer>.fromOpaque(info).takeUnretainedValue()
observer.updatePowerSetting()
}, &context),
SCDynamicStoreSetNotificationKeys(store, [Private.kIOPMDynamicStoreSettingsKey] as CFArray, nil)
else {
return nil
}
let source = SCDynamicStoreCreateRunLoopSource(nil, store, 0)
CFRunLoopAddSource(CFRunLoopGetMain(), source, CFRunLoopMode.commonModes)
return store
}

private func updatePowerSetting() {
handlersQueue.sync { [weak self] in
self?.handlers.values.forEach { $0() }
}
}

func removeHandler(_ id: UUID) {
handlersQueue.sync { [weak self] in
self?.handlers[id] = nil
}
}

func addHandler(_ id: UUID, _ handler: @escaping () -> Void) {
handlersQueue.sync { [weak self] in
self?.handlers[id] = handler
}
}
}
}
44 changes: 44 additions & 0 deletions BatFiKit/Sources/L10n/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,17 @@
}
}
},
"battery_info.label.additional.auto_power_mode" : {
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Auto"
}
}
}
},
"battery_info.label.additional.battery_capacity" : {
"extractionState" : "extracted_with_value",
"localizations" : {
Expand Down Expand Up @@ -494,6 +505,39 @@
}
}
},
"battery_info.label.additional.high_power_mode" : {
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "High"
}
}
}
},
"battery_info.label.additional.low_power_mode" : {
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Low"
}
}
}
},
"battery_info.label.additional.power_mode" : {
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Power Mode"
}
}
}
},
"battery_info.label.additional.power_source" : {
"extractionState" : "extracted_with_value",
"localizations" : {
Expand Down
8 changes: 8 additions & 0 deletions BatFiKit/Sources/L10n/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ public enum L10n {
public static let powerSource = String(localized: "battery_info.label.additional.power_source", defaultValue: "Power Source", bundle: Bundle.module)
/// Temperature
public static let temperature = String(localized: "battery_info.label.additional.temperature", defaultValue: "Temperature", bundle: Bundle.module)
/// Power Mode
public static let powerMode = String(localized: "battery_info.label.additional.power_mode", defaultValue: "Power Mode", bundle: Bundle.module)
/// Low
public static let lowPowerMode = String(localized: "battery_info.label.additional.low_power_mode", defaultValue: "Low", bundle: Bundle.module)
/// Auto
public static let autoPowerMode = String(localized: "battery_info.label.additional.auto_power_mode", defaultValue: "Auto", bundle: Bundle.module)
/// High
public static let highPowerMode = String(localized: "battery_info.label.additional.high_power_mode", defaultValue: "High", bundle: Bundle.module)
}
public enum Main {
/// Battery
Expand Down
Loading

0 comments on commit fc5d74c

Please sign in to comment.