diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/ProvisioningProfile.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/ProvisioningProfile.java index a79d08430..b13b1c196 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/ProvisioningProfile.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/ProvisioningProfile.java @@ -156,6 +156,9 @@ public String getAppIdPrefix() { return appIdPrefix; } + public String getAppId () { + return appId; + } public Date getCreationDate() { return creationDate; } diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/IDevice.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/IDevice.java index 08edfe0d5..49706d23d 100755 --- a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/IDevice.java +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/IDevice.java @@ -150,6 +150,14 @@ public static String[] listUdids() { } } + public static boolean isConnected (String uuid) { + String[] strings = listUdids(); + for (String string : strings) { + if (string.equalsIgnoreCase(uuid)) return true; + } + return false; + } + /** * Registers a new {@link EventListener} which will be called when devices * are added and removed. diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.form b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.form index 796d4db6f..67e6d182e 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.form +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.form @@ -2,7 +2,7 @@
- + @@ -20,7 +20,7 @@ - + @@ -52,7 +52,9 @@ - + + + @@ -60,9 +62,13 @@ - + + + - + + + @@ -84,14 +90,44 @@ - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -178,7 +214,7 @@ - + @@ -186,7 +222,9 @@ - + + + @@ -194,10 +232,48 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java index 816053147..cf8774323 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java @@ -16,10 +16,11 @@ */ package org.robovm.idea.running; +import com.dd.plist.*; import com.intellij.openapi.module.Module; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.SettingsEditor; -import com.intellij.ui.components.JBTextField; +import com.intellij.ui.JBColor; import org.jetbrains.annotations.NotNull; import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; @@ -28,13 +29,20 @@ import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.compiler.target.ios.ProvisioningProfile; import org.robovm.compiler.target.ios.SigningIdentity; +import org.robovm.compiler.util.InfoPList; +import org.robovm.compiler.util.PList; import org.robovm.idea.RoboVmPlugin; import org.robovm.idea.running.RoboVmRunConfiguration.EntryType; +import org.robovm.libimobiledevice.IDevice; +import org.xml.sax.SAXException; import javax.swing.*; -import java.util.HashSet; +import javax.xml.parsers.ParserConfigurationException; +import java.awt.*; +import java.io.IOException; +import java.text.ParseException; +import java.util.*; import java.util.List; -import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -52,6 +60,7 @@ public class RoboVmIOSRunConfigurationSettingsEditor extends SettingsEditor module; + private JLabel bundleLabel; private JRadioButton attachedDeviceRadioButton; private JRadioButton simulatorRadioButton; private JComboBox simType; @@ -61,7 +70,8 @@ public class RoboVmIOSRunConfigurationSettingsEditor extends SettingsEditor deviceArch; private JTextArea args; private JCheckBox pairedWatch; - private JBTextField targetDeviceUDID; + private JComboBox targetDeviceUDID; + private JTextPane errors; // copy of data that is time consuming to fetch (fetched only once when dialog is created) private List roboVmModules; @@ -72,10 +82,17 @@ public class RoboVmIOSRunConfigurationSettingsEditor extends SettingsEditor signingIdentities; private final SigningIdentityDecorator signingIdentityAuto = new SigningIdentityDecorator(AUTO_SIGNING_IDENTITY, EntryType.AUTO); + + private List connectedDevices; private boolean moduleHasWatchApp; // true if editor internally updating data and listeners should ignore the events private boolean updatingData; + private ArrayList provisioningProfileErrors = new ArrayList<>(); + private ArrayList signingIdentityErrors = new ArrayList<>(); + private ArrayList deviceIdentityErrors = new ArrayList<>(); + private IDevice.EventListener ideviceListener; + @NotNull @Override protected JComponent createEditor() { @@ -83,6 +100,7 @@ protected JComponent createEditor() { populateDeviceArch(); populateSigningIdentities(); populateProvisioningProfiles(); + populateDevices(); populateSimulators(); roboVmModules = null; // will be populated once modules list is known @@ -93,10 +111,40 @@ protected JComponent createEditor() { } }); module.addActionListener(e -> { - if (!updatingData) + + if (!updatingData) { updatePairedWatch(true, null); + } + + checkProfilesCertsAndDeviceCompat(); + } ); + provisioningProfile.addActionListener(e -> { + checkProfilesCertsAndDeviceCompat(); + }); + + signingIdentity.addActionListener(e -> { + checkProfilesCertsAndDeviceCompat(); + }); + + + ideviceListener = new IDevice.EventListener() { + @Override + public void deviceAdded(String udid) { + populateDevices(); + checkProfilesCertsAndDeviceCompat(); + } + + @Override + public void deviceRemoved(String udid) { + populateDevices(); + checkProfilesCertsAndDeviceCompat(); + } + }; + IDevice.addEventListener(ideviceListener); + + return panel; } @@ -112,7 +160,7 @@ protected void resetEditorFrom(@NotNull RoboVmRunConfiguration config) { deviceArch.setSelectedItem(config.getDeviceArch()); signingIdentity.setSelectedItem(getSigningIdentityFromConfig(config)); provisioningProfile.setSelectedItem(getProvisioningProfileFromConfig(config)); - targetDeviceUDID.setText(config.getTargetDeviceUDID()); + targetDeviceUDID.setSelectedItem(config.getTargetDeviceUDID()); attachedDeviceRadioButton.setSelected(config.getTargetType() == RoboVmRunConfiguration.TargetType.Device || simType.getItemCount() == 0); args.setText(config.getArguments()); } finally { @@ -145,7 +193,7 @@ protected void applyEditorTo(@NotNull RoboVmRunConfiguration config) throws Conf config.setSigningIdentity(Decorator.from(signingIdentity).id); config.setProvisioningProfileType(Decorator.from(provisioningProfile).entryType); config.setProvisioningProfile(Decorator.from(provisioningProfile).id); - config.setTargetDeviceUDID(targetDeviceUDID.getText()); + config.setTargetDeviceUDID(Decorator.from(targetDeviceUDID).id); // simulator related config.setSimulatorArch((CpuArch) simArch.getSelectedItem()); config.setSimulatorType(Decorator.from(simType).entryType); @@ -223,6 +271,13 @@ private void populateProvisioningProfiles() { this.provisioningProfiles.forEach(t -> provisioningProfile.addItem(t)); } + private void populateDevices () { + this.connectedDevices = Arrays.stream(IDevice.listUdids()).map(IDeviceDecorator::new).collect(Collectors.toList()); + + targetDeviceUDID.removeAllItems(); + this.connectedDevices.forEach(d -> targetDeviceUDID.addItem(d)); + } + private void populateSimulators() { this.simDeviceTypes = DeviceType.listDeviceTypes().stream() .map(SimTypeDecorator::new) @@ -343,6 +398,17 @@ private void updateSimArchs(SimTypeDecorator simulator) { simArch.setSelectedItem(arch); } + private void checkProfilesCertsAndDeviceCompat() { + markCompatibleProfiles(); + markCompatibleCertificates(); + markCompatibleDevices(); + + checkSelectedProvisioningProfile(); + checkSelectedSigningIdentity(); + checkSelectedDevice(); + updateErrors(); + } + private void updatePairedWatch(boolean moduleChanged, Boolean valueToSet) { boolean visible; boolean enabled; @@ -354,6 +420,20 @@ private void updatePairedWatch(boolean moduleChanged, Boolean valueToSet) { Decorator moduleSelected = Decorator.from(module); Config config = moduleSelected != null ? RoboVmPlugin.loadRawModuleConfig(moduleSelected.data) : null; moduleHasWatchApp = config != null && config.getWatchKitApp() != null; + + + + if (config != null) { + Properties properties = config.getProperties(); + InfoPList infoPList = config.getInfoPList(); + infoPList.parse(properties); + if (infoPList.getBundleIdentifier() != null) { + bundleLabel.setText(infoPList.getBundleIdentifier()); + bundleLabel.setForeground(JBColor.foreground()); + } else { + bundleLabel.setForeground(Color.GRAY); + } + } } if (!moduleHasWatchApp) { @@ -382,6 +462,313 @@ private void updatePairedWatch(boolean moduleChanged, Boolean valueToSet) { pairedWatch.setText(text); } + + private void updateErrors() { + if (provisioningProfileErrors.isEmpty() && signingIdentityErrors.isEmpty() && deviceIdentityErrors.isEmpty()) { + errors.setText(""); + return; + }; + + StringBuilder buffer = new StringBuilder(); + + buffer.append("Errors").append("\n\n"); + + if (!provisioningProfileErrors.isEmpty()) { + buffer.append("Provisioning Profile Errors -> ").append("\n"); + for (String provisioningProfileError : provisioningProfileErrors) { + buffer.append(provisioningProfileError).append("\n"); + } + buffer.append("\n"); + } + if (!signingIdentityErrors.isEmpty()) { + buffer.append("Certificate Errors -> ").append("\n"); + for (String certificateError : signingIdentityErrors) { + buffer.append(certificateError).append("\n"); + } + buffer.append("\n"); + } + if (!deviceIdentityErrors.isEmpty()) { + buffer.append("Device Errors -> ").append("\n"); + for (String deviceError : deviceIdentityErrors) { + buffer.append(deviceError).append("\n"); + } + buffer.append("\n"); + } + errors.setText(buffer.toString()); + + } + + private void checkSelectedProvisioningProfile () { + Decorator selectedProvisioningDecorator = Decorator.from(provisioningProfile); + if (selectedProvisioningDecorator != null && selectedProvisioningDecorator.data != null) {//Perhaps none + ProvisioningProfile provisioningProfile = selectedProvisioningDecorator.data; + + Decorator moduleSelected = Decorator.from(module); + Config config = moduleSelected != null ? RoboVmPlugin.loadRawModuleConfig(moduleSelected.data) : null; + if (config != null) { + if (isProvisioningProfileValid(provisioningProfile, config, true)) { + //Its already marked valid or invalid, just want to do this to propagate errors + } + } + } + } + + private void checkSelectedSigningIdentity () { + Decorator selectedSigningIdentity = Decorator.from(signingIdentity); + if (selectedSigningIdentity != null && selectedSigningIdentity.data != null) { + SigningIdentity signingIdent = selectedSigningIdentity.data; + + + Decorator moduleSelected = Decorator.from(module); + Config config = moduleSelected != null ? RoboVmPlugin.loadRawModuleConfig(moduleSelected.data) : null; + if (config != null) { + if (isSigningIdentityValid(signingIdent, config, true)) { + //Its already marked valid or invalid, just want to do this to propagate errors + } + } + } + } + + private void checkSelectedDevice () { + Decorator selectedDevice = Decorator.from(targetDeviceUDID); + + if (selectedDevice != null && selectedDevice.data != null) { + String selectedDeviceUUID = selectedDevice.data; + + Decorator profileSelected = Decorator.from(provisioningProfile); + //ony care about profile + + if (profileSelected != null && profileSelected.data != null) { + if (isDeviceValid(profileSelected, selectedDeviceUUID, true)) { + + } + } + } + } + + private boolean isDeviceValid(Decorator profileSelected, String selectedDeviceUUID, boolean createErrorStrings) { + if (createErrorStrings) { + deviceIdentityErrors.clear(); + } + + NSObject provisionedDevices = profileSelected.data.getDict().get("ProvisionedDevices"); + if (provisionedDevices != null) { + NSArray arrayOfProvisionedDevices = (NSArray) provisionedDevices; + boolean foundDeviceInProfile = false; + for (NSObject nsObject : arrayOfProvisionedDevices.getArray()) { + NSString deviceID = (NSString) nsObject; + if (selectedDeviceUUID.equalsIgnoreCase(deviceID.toString())) { + foundDeviceInProfile = true; + break; + } + } + if (foundDeviceInProfile) { + return true; + } else { + deviceIdentityErrors.add("Device is not in the provisioning profile"); + return false; + } + } + + + + return false; + } + + private boolean isSigningIdentityValid(SigningIdentity signingIdentity, Config config, boolean createErrorStrings) { + if (createErrorStrings) { + signingIdentityErrors.clear(); + } + + boolean currentlyValid = false; + + + Decorator currentSelectedProvisioningProfile = Decorator.from(provisioningProfile); + + boolean isProfileSelectedAndValid = currentSelectedProvisioningProfile != null && currentSelectedProvisioningProfile.data != null; + if (isProfileSelectedAndValid) { + isProfileSelectedAndValid = isProvisioningProfileValid(currentSelectedProvisioningProfile.data, config, false); + } + + if (isProfileSelectedAndValid) { + Set certFingerprints = currentSelectedProvisioningProfile.data.getCertFingerprints(); + if (certFingerprints.contains(signingIdentity.getFingerprint())) { + currentlyValid = true; + } else { + if (createErrorStrings) { + signingIdentityErrors.add("Signing Certificate is not in the selected Provisioning Profile"); + } + } + } else { //Check all of them + for (ProvisioningProfileDecorator provisioningProfile : provisioningProfiles) { + ProvisioningProfile profile = provisioningProfile.data; + if (profile != null) { + if (isProvisioningProfileValid(profile, config, false)) { + Set certFingerprints = profile.getCertFingerprints(); + if (certFingerprints.contains(signingIdentity.getFingerprint())) { + currentlyValid = true; + } + } + } + } + + if (!currentlyValid) { + if (createErrorStrings) { + signingIdentityErrors.add("Signing Certificate is not in any of the Provisioning profiles valid for this app"); + } + } + } + + + + return currentlyValid; + } + + private boolean isProvisioningProfileValid (ProvisioningProfile profile, Config config, boolean createErrorStrings) { + if (createErrorStrings) { + provisioningProfileErrors.clear(); + } + + String appIdPrefix = profile.getAppIdPrefix(); + String appId = profile.getAppId(); + String appIdNoPrefix = appId.split(appIdPrefix + ".")[1]; + + boolean currentlyCompatible = true; + + //Check the id + if (!bundleLabel.getText().equalsIgnoreCase(appIdNoPrefix)) { + currentlyCompatible = false; + if (createErrorStrings) { + provisioningProfileErrors.add("Bundle ID from profile=[" + appIdNoPrefix + "] does not match module Bundle ID [" + bundleLabel.getText() + "]"); + } + } + + //Check if required entitlements are in the profile + if (config.getIosEntitlementsPList() != null) { + try { + NSDictionary entitlementsFromProject = (NSDictionary) PropertyListParser.parse(config.getIosEntitlementsPList()); + + NSDictionary entitlementsFromProfile = profile.getEntitlements(); + + for (String key : entitlementsFromProject.keySet()) { + if (!isEntitlementRequiredToBeInProfile(key)) continue; + + if (!entitlementsFromProfile.containsKey(key)) { + currentlyCompatible = false; + if (createErrorStrings) { + //might have to do valid checks, if real entitlment, and its required to be in profile + provisioningProfileErrors.add("Entitlement " + key + " doesn't exist in provisioning profile"); + } + } + } + + } catch (IOException | PropertyListFormatException | ParseException | ParserConfigurationException | + SAXException e) { + e.printStackTrace(); + } + } + + + return currentlyCompatible; + } + + private static final HashSet entitlementsRequiredInProfile = new HashSet<>(); + static { + entitlementsRequiredInProfile.add("com.apple.developer.networking.wifi-info"); + entitlementsRequiredInProfile.add("com.apple.developer.devicecheck.appattest-environment"); + entitlementsRequiredInProfile.add("com.apple.developer.authentication-services.autofill-credential-provider"); + entitlementsRequiredInProfile.add("com.apple.developer.ClassKit-environment"); + entitlementsRequiredInProfile.add("com.apple.developer.driverkit.communicates-with-drivers"); + entitlementsRequiredInProfile.add("com.apple.developer.usernotifications.communication"); + entitlementsRequiredInProfile.add("com.apple.developer.default-data-protection"); + entitlementsRequiredInProfile.add("com.apple.developer.kernel.extended-virtual-addressing"); + entitlementsRequiredInProfile.add("com.apple.developer.family-controls"); + entitlementsRequiredInProfile.add("com.apple.developer.group-session"); + entitlementsRequiredInProfile.add("com.apple.developer.healthkit"); + entitlementsRequiredInProfile.add("com.apple.developer.homekit"); + entitlementsRequiredInProfile.add("com.apple.developer.networking.HotspotConfiguration"); + entitlementsRequiredInProfile.add("ICloudContainerIdentifiers"); + entitlementsRequiredInProfile.add("com.apple.developer.kernel.increased-memory-limit"); + entitlementsRequiredInProfile.add("inter-app-audio"); + entitlementsRequiredInProfile.add("keychain-access-groups"); + entitlementsRequiredInProfile.add("com.apple.developer.associated-domains.mdm-managed"); + entitlementsRequiredInProfile.add("com.apple.developer.networking.multipath"); + entitlementsRequiredInProfile.add("com.apple.developer.nfc.readersession.formats"); + entitlementsRequiredInProfile.add("com.apple.developer.networking.vpn.api"); + entitlementsRequiredInProfile.add("aps-environment"); + entitlementsRequiredInProfile.add("com.apple.developer.push-to-talk"); + entitlementsRequiredInProfile.add("com.apple.developer.shared-with-you"); + entitlementsRequiredInProfile.add("com.apple.developer.applesignin"); + entitlementsRequiredInProfile.add("com.apple.external-accessory.wireless-configuration"); + entitlementsRequiredInProfile.add("com.apple.developer.weatherkit"); + entitlementsRequiredInProfile.add("com.apple.developer.usernotifications.time-sensitive"); + entitlementsRequiredInProfile.add("com.apple.developer.siri"); + entitlementsRequiredInProfile.add("com.apple.developer.user-fonts"); + entitlementsRequiredInProfile.add("com.apple.developer.associated-domains"); + entitlementsRequiredInProfile.add("com.apple.security.application-groups"); + entitlementsRequiredInProfile.add("com.apple.developer.in-app-payments"); + entitlementsRequiredInProfile.add("com.apple.developer.networking.networkextension"); + + } + private boolean isEntitlementRequiredToBeInProfile(String key) { + return entitlementsRequiredInProfile.contains(key); + } + + + private void markCompatibleProfiles() { + Decorator moduleSelected = Decorator.from(module); + Config config = moduleSelected != null ? RoboVmPlugin.loadRawModuleConfig(moduleSelected.data) : null; + if (config != null) { + for (ProvisioningProfileDecorator profile : provisioningProfiles) { + if (isProvisioningProfileValid(profile.data, config, false)) { + profile.setCurrentlyCompatible(true); + } else { + profile.setCurrentlyCompatible(false); + } + } + } + } + + private void markCompatibleCertificates () { + Decorator moduleSelected = Decorator.from(module); + Config config = moduleSelected != null ? RoboVmPlugin.loadRawModuleConfig(moduleSelected.data) : null; + if (config != null) { + for (SigningIdentityDecorator identity : signingIdentities) { + if (isSigningIdentityValid(identity.data, config, false)) { + identity.setCurrentlyCompatible(true); + } else { + identity.setCurrentlyCompatible(false); + } + } + } + } + + private void markCompatibleDevices () { + Decorator moduleSelected = Decorator.from(module); + Config config = moduleSelected != null ? RoboVmPlugin.loadRawModuleConfig(moduleSelected.data) : null; + if (config != null) { + for (ProvisioningProfileDecorator profile : provisioningProfiles) { + if (isProvisioningProfileValid(profile.data, config, false)) { + //Check over all devices and see if the device is valid + for (IDeviceDecorator connectedDevice : connectedDevices) { + String uuid = connectedDevice.data; + + if (isDeviceValid(profile, uuid, false)) { + connectedDevice.setCurrentlyCompatible(true); + } else { + connectedDevice.setCurrentlyCompatible(false); + } + + } + } + } + } else { + connectedDevices.forEach(d -> { + d.setCurrentlyCompatible(true); + }); + } + } + /** * helper to build exception with quick fix action */ @@ -447,10 +834,51 @@ public String toString() { } } + /** + * decorator for connected device + */ + private static class IDeviceDecorator extends Decorator { + + private boolean currentlyCompatible = true; + IDeviceDecorator (String title, EntryType entryType) { + super(null, null, title, entryType); + } + + IDeviceDecorator(String deviceIdentifier) { + super(deviceIdentifier, deviceIdentifier, deviceIdentifier, EntryType.ID); + } + + public void setCurrentlyCompatible(boolean currentlyCompatible) { + this.currentlyCompatible = currentlyCompatible; + } + + + @Override + public String toString() { + boolean offline = !IDevice.isConnected(id); + if (currentlyCompatible && !offline) { + return name; + } else { + String nameBuffer = ""; + nameBuffer += id; + if (currentlyCompatible) { + nameBuffer += " [Incompatible]"; + } + if (offline) { + nameBuffer += " [Offline]"; + } + return nameBuffer; + } + } + } + + /** * decorator for singing identity */ private static class SigningIdentityDecorator extends Decorator { + + private boolean currentlyCompatible = true; SigningIdentityDecorator(String title, EntryType entryType) { super(null, null, title, entryType); } @@ -459,9 +887,17 @@ private static class SigningIdentityDecorator extends Decorator super(identity, identity.getFingerprint(), identity.getName(), EntryType.ID); } + public void setCurrentlyCompatible(boolean currentlyCompatible) { + this.currentlyCompatible = currentlyCompatible; + } + @Override public String toString() { - return name; + if (currentlyCompatible) { + return name; + } else { + return name + " [Incompatible]"; + } } } @@ -469,6 +905,8 @@ public String toString() { * decorator for provisioning profile */ private static class ProvisioningProfileDecorator extends Decorator { + + private boolean currentlyCompatible = true; ProvisioningProfileDecorator(String title, EntryType entryType) { super(null, null, title, entryType); } @@ -477,9 +915,17 @@ private static class ProvisioningProfileDecorator extends Decorator