Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add power mode info #8

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
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
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