Skip to content

Commit

Permalink
macapp: prevent screen capture nags after permission granted
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredh159 committed Jan 3, 2025
1 parent 4766529 commit d480e80
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 10 deletions.
19 changes: 18 additions & 1 deletion macapp/App/Sources/App/AppReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ struct AppReducer: Reducer, Sendable {
case .startProtecting(let user):
let onboardingWindowOpen = state.onboarding.windowOpen
return .merge(

.exec { [filterVersion = state.filter.version] send in
await self.api.setUserToken(user.token)
guard self.network.isConnected() else { return }
Expand Down Expand Up @@ -173,9 +172,18 @@ struct AppReducer: Reducer, Sendable {

.exec { _ in
try await self.app.startRelaunchWatcher()
},

.exec { _ in
await self.preventScreenCaptureNag()
}
)

case .heartbeat(.everySixHours):
return .exec { _ in
await self.preventScreenCaptureNag()
}

case .focusedNotification(let notification):
// dismiss windows/dropdowns so notification is visible, i.e. "focused"
state.adminWindow.windowOpen = false
Expand Down Expand Up @@ -272,4 +280,13 @@ struct AppReducer: Reducer, Sendable {
OnboardingFeature.Reducer()
}
}

func preventScreenCaptureNag() async {
switch await self.device.preventScreenCaptureNag() {
case .success:
break
case .failure(let error):
unexpectedError(id: "3d2a5573", detail: error.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Gertie

struct MacOSVersion: Sendable {
enum Name: String, Codable {
Expand All @@ -14,8 +15,8 @@ struct MacOSVersion: Sendable {
let minor: Int
let patch: Int

var semver: String {
"\(self.major).\(self.minor).\(self.patch)"
var semver: Semver {
.init(major: self.major, minor: self.minor, patch: self.patch)
}

var name: Name {
Expand All @@ -31,7 +32,7 @@ struct MacOSVersion: Sendable {
}

var description: String {
"\(self.name.rawValue)@\(self.semver)"
"\(self.name.rawValue)@\(self.semver.string)"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Foundation

@Sendable func _preventScreenCaptureNag() async -> Result<Void, AppError> {
let macosVersion = macOSVersion().semver
guard macosVersion.major >= 15 else {
return .success(())
}

let path = approvalFilepath()
switch loadPlist(at: path) {
case .failure(let error):
return .failure(error)
case .success(var plist):
// NB: fileformat changed between 15.0 and 15.1, see:
// https://github.com/gertrude-app/project/issues/334#issuecomment-2568295348
if macosVersion < .init("15.1.0")! {
plist["/Applications/Gertrude.app/Contents/MacOS/Gertrude"] = Date() + .days(90)
} else {
let value: [String: Any] = [
"kScreenCaptureAlertableUsageCount": Int(1),
"kScreenCaptureApprovalLastAlerted": Date() + .days(90),
"kScreenCaptureApprovalLastUsed": Date() + .days(90),
"kScreenCapturePrivacyHintDate": Date() + .days(90),
]
plist["com.netrivet.gertrude.app"] = value
}
return write(plist, to: path)
}
}

private func write(_ plist: [String: Any], to url: URL) -> Result<Void, AppError> {
do {
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try data.write(to: url)
return .success(())
} catch {
return .failure(.init(
oslogging: "error writing plist to file: \(error)",
context: "DeviceClient.preventScreenCaptureNag"
))
}
}

private func loadPlist(at url: URL) -> Result<[String: Any], AppError> {
do {
let data = try Data(contentsOf: url)
do {
guard let plist = try PropertyListSerialization
.propertyList(from: data, options: [], format: nil) as? [String: Any] else {
return .failure(.init(
oslogging: "got nil casting Data to [String: Any]",
context: "DeviceClient.preventScreenCaptureNag"
))
}
return .success(plist)
} catch {
return .failure(.init(
oslogging: "error casting Data to [String: Any]: \(error)",
context: "DeviceClient.preventScreenCaptureNag"
))
}
} catch {
return .failure(.init(
oslogging: "error reading Data from plist file: \(error)",
context: "DeviceClient.preventScreenCaptureNag"
))
}
}

private func approvalFilepath() -> URL {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let path = "\(home)/Library/Group Cont" +
"ainers/gro" + "up.com.apple.repl" +
"ayd/Screen" + "CaptureApp" + "rovals.plist"
return URL(fileURLWithPath: path)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct DeviceClient: Sendable {
var openSystemPrefs: @Sendable (SystemPrefsLocation) async -> Void
var openWebUrl: @Sendable (URL) async -> Void
var osVersion: @Sendable () -> MacOSVersion
var preventScreenCaptureNag: @Sendable () async -> Result<Void, AppError>
var quitBrowsers: @Sendable ([BrowserMatch]) async -> Void
var requestNotificationAuthorization: @Sendable () async -> Void
var runningAppFromPid: @Sendable (pid_t) -> RunningApp?
Expand Down Expand Up @@ -50,6 +51,7 @@ extension DeviceClient: DependencyKey {
openSystemPrefs: openSystemPrefs(at:),
openWebUrl: { NSWorkspace.shared.open($0) },
osVersion: { macOSVersion() },
preventScreenCaptureNag: _preventScreenCaptureNag,
quitBrowsers: quitAllBrowsers,
requestNotificationAuthorization: requestNotificationAuth,
runningAppFromPid: { .init(pid: $0) },
Expand Down Expand Up @@ -92,6 +94,10 @@ extension DeviceClient: TestDependencyKey {
"DeviceClient.osVersion",
placeholder: .init(major: 15, minor: 0, patch: 0)
),
preventScreenCaptureNag: unimplemented(
"DeviceClient.preventScreenCaptureNag",
placeholder: .success(())
),
quitBrowsers: unimplemented("DeviceClient.quitBrowsers"),
requestNotificationAuthorization: unimplemented(
"DeviceClient.requestNotificationAuthorization"
Expand Down Expand Up @@ -125,6 +131,7 @@ extension DeviceClient: TestDependencyKey {
openSystemPrefs: { _ in },
openWebUrl: { _ in },
osVersion: { .init(major: 14, minor: 0, patch: 0) },
preventScreenCaptureNag: { .success(()) },
quitBrowsers: { _ in },
requestNotificationAuthorization: {},
runningAppFromPid: { _ in nil },
Expand Down
26 changes: 25 additions & 1 deletion macapp/App/Sources/App/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ClientInterfaces
import ComposableArchitecture
import Foundation
import MacAppRoute
import os.log

typealias FeatureReducer = Reducer

Expand Down Expand Up @@ -71,7 +72,7 @@ public extension ApiClient {
appVersion: appClient.installedVersion() ?? "unknown",
filterVersion: filterVersion,
userIsAdmin: device.currentMacOsUserType() == .admin,
osVersion: device.osVersion().semver,
osVersion: device.osVersion().semver.string,
pendingFilterSuspension: pendingFilterSuspension,
pendingUnlockRequests: pendingUnlockRequests,
namedApps: sendNamedApps ? device.listRunningApps().filter(\.hasName) : nil
Expand All @@ -83,3 +84,26 @@ public extension ApiClient {
extension URL {
static let contact = URL(string: "https://gertrude.app/contact")!
}

struct AppError: Error, Equatable, Sendable {
var message: String

init(_ message: String) {
self.message = message
}

init(oslogging message: String, context: String? = nil) {
if let context {
os_log("[G•] AppError context: %{public}s, message: %{public}s", context, message)
} else {
os_log("[G•] AppError message: %{public}s", message)
}
self.message = message
}
}

extension AppError: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.message = value
}
}
1 change: 0 additions & 1 deletion macapp/App/Sources/Relauncher/RelauncherClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import AppKit
import Darwin
import Dependencies
import Foundation
import os.log

public struct RelauncherClient: Sendable {
public var commandLineArgs: @Sendable () -> [String]
Expand Down
14 changes: 14 additions & 0 deletions macapp/App/Tests/AppTests/AppReducerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ final class AppReducerTests: XCTestCase {
store.deps.app.startRelaunchWatcher = startRelaunchWatcher.fn
store.deps.device.boottime = { .reference - 60 }
store.deps.date = .constant(.reference)
let preventScreenCaptureNag = mock(always: Result<Void, AppError>.success(()))
store.deps.device.preventScreenCaptureNag = preventScreenCaptureNag.fn

await store.send(.application(.didFinishLaunching))

Expand All @@ -49,6 +51,7 @@ final class AppReducerTests: XCTestCase {
await expect(setUserToken.calls).toEqual([UserData.mock.token])
await expect(enableLaunchAtLogin.calls.count).toEqual(1)
await expect(startRelaunchWatcher.calls.count).toEqual(1)
await expect(preventScreenCaptureNag.calls.count).toEqual(1)

let prevUser = store.state.user.data

Expand Down Expand Up @@ -115,6 +118,17 @@ final class AppReducerTests: XCTestCase {
await expect(connectionEstablished).toEqual(true)
}

@MainActor
func testPreventsScreenCaptureNagEverySixHours() async {
let (store, _) = AppReducer.testStore()
let preventScreenCaptureNag = mock(always: Result<Void, AppError>.success(()))
store.deps.device.preventScreenCaptureNag = preventScreenCaptureNag.fn
await store.send(.heartbeat(.everySixHours))
await expect(preventScreenCaptureNag.calls.count).toEqual(1)
await store.send(.heartbeat(.everySixHours))
await expect(preventScreenCaptureNag.calls.count).toEqual(2)
}

@MainActor
func testDidFinishLaunching_NoPersistentUser() async {
let (store, _) = AppReducer.testStore()
Expand Down
4 changes: 2 additions & 2 deletions macapp/Xcode/Gertrude/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.6.1</string>
<string>2.6.2</string>
<key>CFBundleVersion</key>
<string>2.6.1</string>
<string>2.6.2</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key>
Expand Down
4 changes: 2 additions & 2 deletions macapp/Xcode/GertrudeFilterExtension/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.6.1</string>
<string>2.6.2</string>
<key>CFBundleVersion</key>
<string>2.6.1</string>
<string>2.6.2</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
Expand Down
2 changes: 2 additions & 0 deletions macapp/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
- app blocking
- `2.6.1` (canary as of 12/27/24)
- filter polices app, blocks if awol
- `2.6.1` (canary as of 1/3/25)
- bypasses sequioa screen capture nag

## Sparkle Releases

Expand Down

0 comments on commit d480e80

Please sign in to comment.