View Javadoc
1   /*
2    * Copyright 2025 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.software.os.mac;
6   
7   import static oshi.jna.platform.mac.CoreFoundation.CFDateFormatterStyle.kCFDateFormatterShortStyle;
8   
9   import java.util.ArrayList;
10  import java.util.HashMap;
11  import java.util.List;
12  import java.util.LinkedHashSet;
13  import java.util.LinkedHashMap;
14  import java.util.Map;
15  import java.util.Set;
16  
17  import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
18  import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
19  
20  import oshi.jna.platform.mac.CoreFoundation;
21  import oshi.jna.platform.mac.CoreFoundation.CFDateFormatter;
22  import oshi.jna.platform.mac.CoreFoundation.CFDateFormatterStyle;
23  import oshi.jna.platform.mac.CoreFoundation.CFLocale;
24  import oshi.software.os.ApplicationInfo;
25  import oshi.util.Constants;
26  import oshi.util.ExecutingCommand;
27  import oshi.util.ParseUtil;
28  
29  public final class MacInstalledApps {
30  
31      private static final String COLON = ":";
32      private static final CoreFoundation CF = CoreFoundation.INSTANCE;
33  
34      private MacInstalledApps() {
35      }
36  
37      public static List<ApplicationInfo> queryInstalledApps() {
38          List<String> output = ExecutingCommand.runNative("system_profiler SPApplicationsDataType");
39          return parseMacAppInfo(output);
40      }
41  
42      private static List<ApplicationInfo> parseMacAppInfo(List<String> lines) {
43          Set<ApplicationInfo> appInfoSet = new LinkedHashSet<>();
44          String appName = null;
45          Map<String, String> appDetails = null;
46          boolean collectingAppDetails = false;
47          String dateFormat = getLocaleDateTimeFormat(kCFDateFormatterShortStyle);
48  
49          for (String line : lines) {
50              line = line.trim();
51  
52              // Check for app name, ends with ":"
53              if (line.endsWith(COLON)) {
54                  // When app and appDetails are not empty then we reached the next app, add it to the list
55                  if (appName != null && !appDetails.isEmpty()) {
56                      appInfoSet.add(createAppInfo(appName, appDetails, dateFormat));
57                  }
58  
59                  // store app name and proceed with collecting app details
60                  appName = line.substring(0, line.length() - 1);
61                  appDetails = new HashMap<>();
62                  collectingAppDetails = true;
63                  continue;
64              }
65  
66              // Process app details
67              if (collectingAppDetails && line.contains(COLON)) {
68                  int colonIndex = line.indexOf(COLON);
69                  String key = line.substring(0, colonIndex).trim();
70                  String value = line.substring(colonIndex + 1).trim();
71                  appDetails.put(key, value);
72              }
73          }
74  
75          return new ArrayList<>(appInfoSet);
76      }
77  
78      private static ApplicationInfo createAppInfo(String name, Map<String, String> details, String dateFormat) {
79          String obtainedFrom = ParseUtil.getValueOrUnknown(details, "Obtained from");
80          String signedBy = ParseUtil.getValueOrUnknown(details, "Signed by");
81          String vendor = (obtainedFrom.equals("Identified Developer")) ? signedBy : obtainedFrom;
82  
83          String lastModified = details.getOrDefault("Last Modified", Constants.UNKNOWN);
84          long lastModifiedEpoch = ParseUtil.parseDateToEpoch(lastModified, dateFormat);
85  
86          // Additional info map
87          Map<String, String> additionalInfo = new LinkedHashMap<>();
88          additionalInfo.put("Kind", ParseUtil.getValueOrUnknown(details, "Kind"));
89          additionalInfo.put("Location", ParseUtil.getValueOrUnknown(details, "Location"));
90          additionalInfo.put("Get Info String", ParseUtil.getValueOrUnknown(details, "Get Info String"));
91  
92          return new ApplicationInfo(name, ParseUtil.getValueOrUnknown(details, "Version"), vendor, lastModifiedEpoch,
93                  additionalInfo);
94      }
95  
96      private static String getLocaleDateTimeFormat(CFDateFormatterStyle style) {
97          CFIndex styleIndex = style.index();
98          CFLocale locale = CF.CFLocaleCopyCurrent();
99          try {
100             CFDateFormatter formatter = CF.CFDateFormatterCreate(null, locale, styleIndex, styleIndex);
101             if (formatter == null) {
102                 return "";
103             }
104             try {
105                 CFStringRef format = CF.CFDateFormatterGetFormat(formatter);
106                 return (format == null) ? "" : format.stringValue();
107             } finally {
108                 CF.CFRelease(formatter);
109             }
110         } finally {
111             CF.CFRelease(locale);
112         }
113     }
114 }