feat(macos): add sandboxed launch at login for App Store version (#1793)

This commit is contained in:
Shlomo
2024-09-25 15:14:08 +03:00
committed by GitHub
parent dbd588beba
commit 65aac3c965
7 changed files with 126 additions and 42 deletions

View File

@@ -105,7 +105,11 @@ Future<RefenaContainer> preInit(List<String> args) async {
if (args.contains(startHiddenFlag)) {
// keep this app hidden
startHidden = true;
} else if (defaultTargetPlatform == TargetPlatform.macOS) {
startHidden = await isLaunchedAsLoginItem() && await getLaunchAtLoginMinimized();
}
if (startHidden) {
unawaited(hideToTray());
} else {
await WindowManager.instance.show();

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:flutter/foundation.dart';
@@ -30,8 +28,6 @@ import 'package:refena_flutter/refena_flutter.dart';
import 'package:routerino/routerino.dart';
import 'package:url_launcher/url_launcher.dart';
final _isMacOSSandboxed = defaultTargetPlatform == TargetPlatform.macOS && Platform.environment['APP_SANDBOX_CONTAINER_ID'] != null;
class SettingsTab extends StatelessWidget {
const SettingsTab();
@@ -106,7 +102,7 @@ class SettingsTab extends StatelessWidget {
},
),
],
if (checkPlatformIsDesktop() && !_isMacOSSandboxed) ...[
if (checkPlatformIsDesktop()) ...[
_BooleanEntry(
label: t.settingsTab.general.launchAtStartup,
value: vm.autoStart,

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:localsend_app/util/native/macos_channel.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:win32_registry/win32_registry.dart';
@@ -30,31 +31,8 @@ Terminal=false
file.writeAsStringSync(contents);
return true;
case TargetPlatform.macOS:
final file = File(_getMacOSFilePath(packageInfo.packageName));
file.writeAsStringSync('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${packageInfo.packageName}</string>
<key>AssociatedBundleIdentifiers</key>
<string>${packageInfo.packageName}</string>
<key>ProgramArguments</key>
<array>
<string>${Platform.executable}</string>
${startHidden ? '<string>$startHiddenFlag</string>' : ''}
</array>
<key>RunAtLoad</key>
<true/>
<key>ProcessType</key>
<string>Interactive</string>
<key>StandardErrorPath</key>
<string>/dev/null</string>
<key>StandardOutPath</key>
<string>/dev/null</string>
</dict>
</plist>
''');
await setLaunchAtLogin(true);
await setLaunchAtLoginMinimized(startHidden);
return true;
case TargetPlatform.windows:
_getWindowsRegistryKey().createValue(RegistryValue(
@@ -80,7 +58,7 @@ Future<bool> disableAutoStart() async {
File(_getLinuxFilePath(packageInfo.packageName)).deleteSync();
break;
case TargetPlatform.macOS:
File(_getMacOSFilePath(packageInfo.packageName)).deleteSync();
await setLaunchAtLogin(false);
break;
case TargetPlatform.windows:
_getWindowsRegistryKey().deleteValue(_windowsRegistryKeyValue);
@@ -101,7 +79,7 @@ Future<bool> isAutoStartEnabled() async {
case TargetPlatform.linux:
return File(_getLinuxFilePath(packageInfo.packageName)).existsSync();
case TargetPlatform.macOS:
return File(_getMacOSFilePath(packageInfo.packageName)).existsSync();
return await getLaunchAtLogin();
case TargetPlatform.windows:
return _getWindowsRegistryKey().getValueAsString(_windowsRegistryKeyValue)?.contains(Platform.resolvedExecutable) ?? false;
default:
@@ -119,11 +97,7 @@ Future<bool> isAutoStartHidden() async {
}
return file.readAsStringSync().contains(startHiddenFlag);
case TargetPlatform.macOS:
final file = File(_getMacOSFilePath(packageInfo.packageName));
if (!file.existsSync()) {
return false;
}
return file.readAsStringSync().contains(startHiddenFlag);
return await getLaunchAtLoginMinimized();
case TargetPlatform.windows:
return _getWindowsRegistryKey().getValueAsString(_windowsRegistryKeyValue)?.contains(startHiddenFlag) ?? false;
default:
@@ -144,7 +118,3 @@ RegistryKey _getWindowsRegistryKey() {
String _getLinuxFilePath(String appName) {
return '${Platform.environment['HOME']}/.config/autostart/$appName.desktop';
}
String _getMacOSFilePath(String appName) {
return '${Platform.environment['HOME']}/Library/LaunchAgents/$appName.plist';
}

View File

@@ -27,6 +27,46 @@ Future<void> updateDockProgress(double progress) async {
await _methodChannel.invokeMethod('updateDockProgress', progress);
}
Future<void> setLaunchAtLogin(bool value) async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return;
}
await _methodChannel.invokeMethod('setLaunchAtLogin', value);
}
Future<bool> getLaunchAtLogin() async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return false;
}
return await _methodChannel.invokeMethod('getLaunchAtLogin');
}
Future<void> setLaunchAtLoginMinimized(bool value) async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return;
}
await _methodChannel.invokeMethod('setLaunchAtLoginMinimized', value);
}
Future<bool> getLaunchAtLoginMinimized() async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return false;
}
return await _methodChannel.invokeMethod('getLaunchAtLoginMinimized');
}
Future<bool> isLaunchedAsLoginItem() async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return false;
}
return await _methodChannel.invokeMethod('isLaunchedAsLoginItem');
}
Future<void> setDockIcon(TaskbarIcon icon) async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return;

View File

@@ -30,6 +30,7 @@
0543E87B2C8A93A9004AA8B7 /* ContentDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0543E87A2C8A93A9004AA8B7 /* ContentDropView.swift */; };
05BAA8D02C8CE248003CFCF1 /* SecurityScopedResourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BAA8CF2C8CE248003CFCF1 /* SecurityScopedResourceManager.swift */; };
05CD1E642C98F2220044F24D /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = 05CD1E632C98F2220044F24D /* DockProgress */; };
05F86B372C9B80BD000EB4BA /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 05F86B362C9B80BD000EB4BA /* LaunchAtLogin */; };
0F663CE30FCD07225E44A808 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F8CCFF0FAB783405F067709 /* Pods_Runner.framework */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
@@ -123,6 +124,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
05F86B372C9B80BD000EB4BA /* LaunchAtLogin in Frameworks */,
05CD1E642C98F2220044F24D /* DockProgress in Frameworks */,
0F663CE30FCD07225E44A808 /* Pods_Runner.framework in Frameworks */,
);
@@ -261,6 +263,7 @@
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
05F86B382C9B8118000EB4BA /* Copy “Launch at Login Helper” */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
2DA7D291A0AEEC0A0C7CE5C0 /* [CP] Embed Pods Frameworks */,
@@ -275,6 +278,7 @@
name = Runner;
packageProductDependencies = (
05CD1E632C98F2220044F24D /* DockProgress */,
05F86B362C9B80BD000EB4BA /* LaunchAtLogin */,
);
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* LocalSend.app */;
@@ -321,6 +325,7 @@
mainGroup = 33CC10E42044A3C60003C045;
packageReferences = (
05CD1E622C98F2220044F24D /* XCRemoteSwiftPackageReference "DockProgress" */,
05F86B352C9B80BD000EB4BA /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */,
);
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
@@ -354,6 +359,25 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
05F86B382C9B8118000EB4BA /* Copy “Launch at Login Helper” */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Copy “Launch at Login Helper”";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${BUILT_PRODUCTS_DIR}/LaunchAtLogin_LaunchAtLogin.bundle/Contents/Resources/copy-helper-swiftpm.sh\"\n";
};
2DA7D291A0AEEC0A0C7CE5C0 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -935,6 +959,14 @@
minimumVersion = 4.3.1;
};
};
05F86B352C9B80BD000EB4BA /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Legacy";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.0.2;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -943,6 +975,11 @@
package = 05CD1E622C98F2220044F24D /* XCRemoteSwiftPackageReference "DockProgress" */;
productName = DockProgress;
};
05F86B362C9B80BD000EB4BA /* LaunchAtLogin */ = {
isa = XCSwiftPackageProductDependency;
package = 05F86B352C9B80BD000EB4BA /* XCRemoteSwiftPackageReference "LaunchAtLogin-Legacy" */;
productName = LaunchAtLogin;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "3e0046e1b0518dc180bd8a7c98fa476d9af079586f505a34aa5b924f90460bda",
"originHash" : "72ae1b64a4d55b124e5d523ea3b7e7432749772550fc8b66a0f17a2caa5d4dc1",
"pins" : [
{
"identity" : "dockprogress",
@@ -9,6 +9,15 @@
"revision" : "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
"version" : "4.3.1"
}
},
{
"identity" : "launchatlogin-legacy",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/LaunchAtLogin-Legacy",
"state" : {
"revision" : "9a894d799269cb591037f9f9cb0961510d4dca81",
"version" : "5.0.2"
}
}
],
"version" : 3

View File

@@ -2,6 +2,7 @@ import Cocoa
import FlutterMacOS
import Defaults
import DockProgress
import LaunchAtLogin
enum DockIcon: CaseIterable {
case regular
@@ -15,6 +16,7 @@ class AppDelegate: FlutterAppDelegate {
private var channel: FlutterMethodChannel?
private var pendingFilesObservation: Defaults.Observation?
private var pendingStringsObservation: Defaults.Observation?
private var isLaunchedAsLoginItem: Bool?
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
// LocalSend handles the close event manually
@@ -30,6 +32,12 @@ class AppDelegate: FlutterAppDelegate {
let localsendBrandColor = NSColor(red: 0, green: 0.392, blue: 0.353, alpha: 0.8) // #00645a
DockProgress.style = .squircle(color: localsendBrandColor)
// source: https://stackoverflow.com/a/19890943
// NOTE: this check must be in applicationDidFinishLaunching, otherwise the `NSAppleEventManager.shared().currentAppleEvent` will be nil
if let event = NSAppleEventManager.shared().currentAppleEvent {
isLaunchedAsLoginItem = (event.eventID == kAEOpenApplication) && (event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem)
}
}
override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
@@ -155,6 +163,26 @@ class AppDelegate: FlutterAppDelegate {
let newIconIndex = call.arguments as! Int
let newIcon = DockIcon.allCases[newIconIndex]
setDockIcon(icon: newIcon)
case "getLaunchAtLogin":
result(LaunchAtLogin.isEnabled)
case "setLaunchAtLogin":
if let launchAtLogin = call.arguments as? Bool {
LaunchAtLogin.isEnabled = launchAtLogin
result(nil)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected a boolean value", details: nil))
}
case "getLaunchAtLoginMinimized":
result(UserDefaults.standard.bool(forKey: "launchAtLoginMinimized"))
case "setLaunchAtLoginMinimized":
if let launchAtLoginMinimized = call.arguments as? Bool {
UserDefaults.standard.set(launchAtLoginMinimized, forKey: "launchAtLoginMinimized")
result(nil)
} else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Expected a boolean value", details: nil))
}
case "isLaunchedAsLoginItem":
result(isLaunchedAsLoginItem)
default:
result(FlutterMethodNotImplemented)
}