Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,6 @@ fastlane/test_output
iOSInjectionProject/

.DS_Store

# Local build and packaging artifacts
dist/
8 changes: 4 additions & 4 deletions MiniSim.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
76C1396A2C849A3F006CD80C /* MenuIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76C139692C849A3F006CD80C /* MenuIcons.swift */; };
76E4451229D4391000039025 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E4451129D4391000039025 /* Onboarding.swift */; };
76E4451429D4403F00039025 /* NSNotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E4451329D4403F00039025 /* NSNotificationName.swift */; };
76F04A11298A5AE000BF9CA3 /* ADB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F04A10298A5AE000BF9CA3 /* ADB.swift */; };
76F04A11298A5AE000BF9CA3 /* Adb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F04A10298A5AE000BF9CA3 /* Adb.swift */; };
76F04A14298A62CF00BF9CA3 /* ShellOut in Frameworks */ = {isa = PBXBuildFile; productRef = 76F04A13298A62CF00BF9CA3 /* ShellOut */; };
76F269852A2A376A00424BDA /* CustomCommandError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F269842A2A376A00424BDA /* CustomCommandError.swift */; };
76F269872A2A39D100424BDA /* Variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F269862A2A39D100424BDA /* Variables.swift */; };
Expand Down Expand Up @@ -221,7 +221,7 @@
76C139692C849A3F006CD80C /* MenuIcons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuIcons.swift; sourceTree = "<group>"; };
76E4451129D4391000039025 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
76E4451329D4403F00039025 /* NSNotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNotificationName.swift; sourceTree = "<group>"; };
76F04A10298A5AE000BF9CA3 /* ADB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ADB.swift; sourceTree = "<group>"; };
76F04A10298A5AE000BF9CA3 /* Adb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Adb.swift; sourceTree = "<group>"; };
76F269842A2A376A00424BDA /* CustomCommandError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCommandError.swift; sourceTree = "<group>"; };
76F269862A2A39D100424BDA /* Variables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variables.swift; sourceTree = "<group>"; };
76F269892A2A40F700424BDA /* SectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeader.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -360,7 +360,7 @@
76BF0AED2C905C43003BE568 /* IOSDeviceService.swift */,
76BF0AE92C905A94003BE568 /* AppleUtils.swift */,
7699511C2C845B1900462287 /* DeviceParser.swift */,
76F04A10298A5AE000BF9CA3 /* ADB.swift */,
76F04A10298A5AE000BF9CA3 /* Adb.swift */,
76B70F832B0D5AB4009D87A4 /* Shell.swift */,
76BF0ADE2C8E01B3003BE568 /* CustomCommandService.swift */,
76BF0AE02C8E027D003BE568 /* DeviceConstants.swift */,
Expand Down Expand Up @@ -670,7 +670,7 @@
76489D5A29BFC60C0070EF03 /* WelcomeView.swift in Sources */,
76F2698A2A2A40F700424BDA /* SectionHeader.swift in Sources */,
7645D4C22982CA9600019227 /* AppDelegate.swift in Sources */,
76F04A11298A5AE000BF9CA3 /* ADB.swift in Sources */,
76F04A11298A5AE000BF9CA3 /* Adb.swift in Sources */,
76730BB9298C1DF80019C680 /* DeviceMenuItem.swift in Sources */,
551B882A2B1385E900B8D325 /* Terminal.swift in Sources */,
767A9CC929C320ED00554193 /* OnboardingButton.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions MiniSim/AppleScript Commands/LaunchDeviceCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class LaunchDeviceCommand: NSScriptCommand {
var devices: [Device] = []
try devices.append(contentsOf: DeviceServiceFactory.getDeviceDiscoveryService(platform: .ios).getDevices())
try devices.append(contentsOf: DeviceServiceFactory.getDeviceDiscoveryService(platform: .android).getDevices())
try devices.append(contentsOf: DeviceServiceFactory.getDeviceDiscoveryService(platform: .harmony).getDevices())

guard let device = devices.first(where: { $0.name == deviceName }) else {
scriptErrorNumber = NSInternalScriptError
Expand Down
6 changes: 3 additions & 3 deletions MiniSim/AppleScript Commands/MiniSim.sdef
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<command name="getDevices" code="MinSDevi">
<cocoa class="MiniSim.GetDevicesCommand"/>
<access-group identifier="*"/>
<parameter code="plat" name="platform" description="Platform to get. Can be either android | ios." type="text">
<parameter code="plat" name="platform" description="Platform to get. Can be android | ios | harmony." type="text">
<cocoa key="platform"/>
</parameter>
<parameter code="devT" name="deviceType" description="DeviceType to get. Can be either physical | virtual" type="text">
Expand All @@ -32,7 +32,7 @@
<command name="getCommands" code="MinSGetc">
<cocoa class="MiniSim.GetCommands"/>
<access-group identifier="*"/>
<parameter code="plat" name="platform" description="Platform to get. Can be either android | ios." type="text">
<parameter code="plat" name="platform" description="Platform to get. Can be android | ios | harmony." type="text">
<cocoa key="platform"/>
</parameter>
<parameter code="devT" name="deviceType" description="DeviceType to get. Can be either physical | virtual" type="text">
Expand All @@ -44,7 +44,7 @@
<command name="executeCommand" code="MinExecu">
<cocoa class="MiniSim.ExecuteCommand"/>
<access-group identifier="*"/>
<parameter code="plat" name="platform" description="Platform to get. Can be either android | ios." type="text">
<parameter code="plat" name="platform" description="Platform to get. Can be android | ios | harmony." type="text">
<cocoa key="platform"/>
</parameter>
<parameter code="comm" name="commandName" description="Name of the command." type="text">
Expand Down
7 changes: 7 additions & 0 deletions MiniSim/Extensions/UserDefaults+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ extension UserDefaults {
static let isOnboardingFinished = "isOnboardingFinished"
static let enableiOSSimulators = "enableiOSSimulators"
static let enableAndroidEmulators = "enableAndroidEmulators"
static let enableHarmonySimulators = "enableHarmonySimulators"
static let didMigrateHarmonySimulators = "didMigrateHarmonySimulators"
static let preferedTerminal = "preferedTerminal"
static let menuImage = "menuImage"
}
Expand Down Expand Up @@ -54,6 +56,11 @@ extension UserDefaults {
set { set(newValue, forKey: Keys.enableAndroidEmulators) }
}

public var enableHarmonySimulators: Bool {
get { bool(forKey: Keys.enableHarmonySimulators) }
set { set(newValue, forKey: Keys.enableHarmonySimulators) }
}

@objc public dynamic var preferedTerminal: String? {
get { string(forKey: Keys.preferedTerminal) }
set { set(newValue, forKey: Keys.preferedTerminal) }
Expand Down
8 changes: 8 additions & 0 deletions MiniSim/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
<string>MiniSim.sdef</string>
<key>NSAppleScriptEnabled</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>MiniSim uses the camera to provide camera input to the HarmonyOS simulator.</string>
<key>NSMicrophoneUsageDescription</key>
<string>MiniSim uses the microphone to provide audio input to the HarmonyOS simulator.</string>
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/okwasniewski/MiniSim/main/appcast.xml</string>
<key>SUEnableAutomaticChecks</key>
<false/>
<key>SUAutomaticallyUpdate</key>
<false/>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleShortVersionString</key>
Expand Down
54 changes: 50 additions & 4 deletions MiniSim/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Menu: NSMenu {
}

func populateDefaultMenu() {
let mainMenuItems = items.filter { $0.target is MiniSim }
var sections: [DeviceListSection] = []

sections.append(.iOSPhysical)
Expand All @@ -47,7 +48,12 @@ class Menu: NSMenu {
sections.append(.androidVirtual)
}

if UserDefaults.standard.enableHarmonySimulators {
sections.append(.harmonyVirtual)
}

if sections.isEmpty {
self.items = mainMenuItems
return
}

Expand All @@ -67,14 +73,15 @@ class Menu: NSMenu {
menuItems.append(menuItem)
menuItems.append(NSMenuItem.separator())
}
self.items = menuItems
self.items = menuItems + mainMenuItems
}

func updateDevicesList() {
let userDefaults = UserDefaults.standard
DeviceServiceFactory.getAllDevices(
android: userDefaults.enableAndroidEmulators && userDefaults.androidHome != nil,
iOS: userDefaults.enableiOSSimulators
iOS: userDefaults.enableiOSSimulators,
harmony: userDefaults.enableHarmonySimulators
) { devices, error in
if let error {
NSAlert.showError(message: error.localizedDescription)
Expand Down Expand Up @@ -118,6 +125,13 @@ class Menu: NSMenu {
}
do {
try device.launch()
if device.platform == .harmony {
// DevEco boots asynchronously. Refresh the state after its
// window/process has had time to initialize.
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.updateDevicesList()
}
}
} catch {
NSAlert.showError(message: error.localizedDescription)
}
Expand All @@ -133,12 +147,14 @@ class Menu: NSMenu {
let deviceItems = items.filter { !sections.contains($0.title) }
let iosDeviceNames = devices.filter { $0.platform == Platform.ios }.map { $0.displayName }
let androidDeviceNames = devices.filter { $0.platform == Platform.android }.map { $0.displayName }
let harmonyDeviceNames = devices.filter { $0.platform == Platform.harmony }.map { $0.displayName }

let iosDevices = deviceItems.filter { iosDeviceNames.contains($0.title) }
let androidDevices = deviceItems.filter { androidDeviceNames.contains($0.title) }

assignKeyEquivalent(devices: iosDevices)
assignKeyEquivalent(devices: androidDevices)
assignKeyEquivalent(devices: deviceItems.filter { harmonyDeviceNames.contains($0.title) })
}

private func assignKeyEquivalent(devices: [NSMenuItem]) {
Expand Down Expand Up @@ -183,6 +199,10 @@ class Menu: NSMenu {
sections.append(.iOSVirtual)
}
sections.append(.iOSPhysical)

if UserDefaults.standard.enableHarmonySimulators {
sections.append(.harmonyVirtual)
}
return sections
}

Expand All @@ -197,6 +217,8 @@ class Menu: NSMenu {
return device.platform == .android && device.type == .physical
case .androidVirtual:
return device.platform == .android && device.type == .virtual
case .harmonyVirtual:
return device.platform == .harmony && device.type == .virtual
}
}
}
Expand Down Expand Up @@ -225,17 +247,39 @@ class Menu: NSMenu {
title: device.displayName,
action: #selector(deviceItemClick),
keyEquivalent: "",
type: device.platform == .ios ? .launchIOS : .launchAndroid,
type: menuItemType(for: device.platform),
deviceFamily: device.deviceFamily
)

menuItem.target = self
menuItem.keyEquivalentModifierMask = device.platform == .android ? [.option] : [.command]
menuItem.keyEquivalentModifierMask = keyEquivalentModifierMask(for: device.platform)
menuItem.submenu = buildSubMenu(for: device)
menuItem.state = device.booted ? .on : .off
return menuItem
}

private func menuItemType(for platform: Platform) -> DeviceMenuItem {
switch platform {
case .ios:
return .launchIOS
case .android:
return .launchAndroid
case .harmony:
return .launchHarmony
}
}

private func keyEquivalentModifierMask(for platform: Platform) -> NSEvent.ModifierFlags {
switch platform {
case .ios:
return [.command]
case .android:
return [.option]
case .harmony:
return [.control]
}
}

private func replaceMenuItem(at index: Int, with newItem: NSMenuItem) {
self.removeItem(at: index)
self.insertItem(newItem, at: index)
Expand Down Expand Up @@ -359,6 +403,8 @@ class Menu: NSMenu {
extension Menu: NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
NotificationCenter.default.post(name: .menuWillOpen, object: nil)
// Rebuild section headers so preference changes take effect immediately.
self.populateDefaultMenu()
self.updateDevicesList()
KeyboardShortcuts.disable(.toggleMiniSim)
}
Expand Down
3 changes: 3 additions & 0 deletions MiniSim/MenuItems/DeviceListSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum DeviceListSection: Int, CaseIterable {
case iOSVirtual
case androidPhysical
case androidVirtual
case harmonyVirtual

var title: String {
switch self {
Expand All @@ -23,6 +24,8 @@ enum DeviceListSection: Int, CaseIterable {
return NSLocalizedString("Android Emulators", comment: "")
case .androidPhysical:
return NSLocalizedString("Android Devices", comment: "")
case .harmonyVirtual:
return NSLocalizedString("HarmonyOS Simulators", comment: "")
}
}
}
3 changes: 3 additions & 0 deletions MiniSim/MenuItems/DeviceMenuItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ enum DeviceMenuItem: Int {

// iOS
case launchIOS = 300

// HarmonyOS
case launchHarmony = 400
}
14 changes: 14 additions & 0 deletions MiniSim/MenuItems/SubMenuItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,20 @@ extension SubMenuItems {

DeleteEmulator()
]
case (.harmony, .virtual):
return [
CopyName(),
CopyID(),

Separator(),

DeleteEmulator()
]
case (.harmony, .physical):
return [
CopyName(),
CopyID()
]
}
}
}
12 changes: 12 additions & 0 deletions MiniSim/MiniSim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,20 @@ class MiniSim: NSObject {
UserDefaults.standard.register(defaults: [
UserDefaults.Keys.enableAndroidEmulators: true,
UserDefaults.Keys.enableiOSSimulators: true,
UserDefaults.Keys.enableHarmonySimulators: true,
UserDefaults.Keys.preferedTerminal: "Terminal"
])

// Older builds initialized this new option to false. Migrate that value
// once so existing installations can discover HarmonyOS automatically;
// subsequent user changes are preserved.
if UserDefaults.standard.object(forKey: UserDefaults.Keys.didMigrateHarmonySimulators) == nil {
UserDefaults.standard.enableHarmonySimulators = true
UserDefaults.standard.set(
true,
forKey: UserDefaults.Keys.didMigrateHarmonySimulators
)
}
}

private func setMenuImage() {
Expand Down
6 changes: 6 additions & 0 deletions MiniSim/Model/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ struct Device: Hashable, Codable {

case .android:
return name

case .harmony:
if let version {
return "\(name) - (\(version))"
}
return name
}
}

Expand Down
1 change: 1 addition & 0 deletions MiniSim/Model/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
enum Platform: String, Codable {
case ios
case android
case harmony
}

// TODO: Remove this type during migration to CoreData.
Expand Down
9 changes: 9 additions & 0 deletions MiniSim/Model/Variables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ enum Variables: String {
case uuid = "$uuid"
case xcrunPath = "$xcrun_path"

// HarmonyOS Specific
case harmonyEmulatorPath = "$harmony_emulator_path"

static var common: [Variables] {
[deviceName]
}
Expand All @@ -31,6 +34,10 @@ enum Variables: String {
[uuid, xcrunPath]
}

static var harmony: [Variables] {
[harmonyEmulatorPath]
}

var description: String {
switch self {
case .deviceName:
Expand All @@ -45,6 +52,8 @@ enum Variables: String {
return NSLocalizedString("Unique identifier of iOS simulator", comment: "")
case .xcrunPath:
return NSLocalizedString("Path to xcrun utility", comment: "")
case .harmonyEmulatorPath:
return NSLocalizedString("Path to the DevEco Studio emulator utility", comment: "")
}
}
}
7 changes: 7 additions & 0 deletions MiniSim/Service/ActionExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class ActionExecutor {
itemName: itemName,
skipConfirmation: skipConfirmation
)
case .harmony:
action = HarmonyActionFactory.createAction(
for: commandTag,
device: device,
itemName: itemName,
skipConfirmation: skipConfirmation
)
}

if action.showQuestionDialog() {
Expand Down
Loading