package org.limewire.ui.swing.util;
import java.awt.FileDialog;
import java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.filechooser.FileFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.service.ErrorService;
import org.limewire.ui.swing.components.LimeJFrame;
import org.limewire.util.CommonUtils;
import org.limewire.util.OSUtils;
import foxtrot.Job;
import foxtrot.Worker;
/**
* A collection of utility methods for OSX.
* These methods should only be called if run from OSX,
* otherwise ClassNotFoundErrors may occur.
*
* Clients may use the method isNativeLibraryLoadedCorrectly()
* to check whether the native library loaded correctly.
* If not, they may choose to disable certain user interface
* features to reflect this state.
*
* <p>
* To determine if the Cocoa Foundation classes are present,
* use the method CommonUtils.isCocoaFoundationAvailable().
*/
public class MacOSXUtils {
/**
* The application bundle identifier for the LimeWire application that is packed into its Info.plist config file.
*/
public static final String LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER = "com.limegroup.gnutella";
/**
* The name of the app that launches.
*/
private static final String APP_NAME = "LimeWire.app";
private static boolean nativeLibraryLoadedCorrectly = false;
private static final Log LOG = LogFactory.getLog(MacOSXUtils.class);
static {
if (OSUtils.isMacOSX()) {
try {
System.loadLibrary("MacOSXUtils");
nativeLibraryLoadedCorrectly = true;
} catch (UnsatisfiedLinkError err) {
ErrorService.error(err, "java.library.path=" + System.getProperty("java.library.path") + "\n\n" + "trace dependencies=" + MacOSXUtils.traceLibraryDependencies("MacOSXUtils.jnilib"));
}
}
}
/**
* This returns a boolean indicating whether an exception occurred when loading the native library.
* @return true if the native library loaded without any errors.
*/
public static boolean isNativeLibraryLoadedCorrectly() {
return nativeLibraryLoadedCorrectly;
}
private MacOSXUtils() {}
/**
* If a given library is not loading for some users on OS-X, this method
* can be used to trace what other libraries this library is dependent
* on and whether those libraries are present on the user's system.
*/
public static String traceLibraryDependencies(String libraryName) {
StringBuffer traceResultsBuffer = new StringBuffer("ls command output: ");
String lsCommand = "ls " + System.getProperty("user.dir");
traceResultsBuffer.append("(").append(lsCommand).append(") ");
traceResultsBuffer.append( getCommandOutput(lsCommand) );
traceResultsBuffer.append( "\n" );
String otoolCommand = "otool -L " + System.getProperty("user.dir") + "/" + libraryName;
traceResultsBuffer.append("otool command output: ");
traceResultsBuffer.append( getCommandOutput(otoolCommand) );
traceResultsBuffer.append( "\n" );
return traceResultsBuffer.toString();
}
/**
* This method runs a system command and returns the command's output as a string.
*
*/
private static String getCommandOutput(String command) {
StringBuffer outputBuffer = new StringBuffer("");
try {
// start the command running
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(command);
// put a BufferedReader on the command output
InputStream inputstream = process.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);
// read the command output
String line;
while ((line = bufferedreader.readLine()) != null) {
outputBuffer.append(line).append("\n");
}
// check for command failure
try {
if (process.waitFor() != 0) {
outputBuffer.append("exit value = ");
outputBuffer.append(process.exitValue());
}
}
catch (InterruptedException e) {
}
} catch (IOException exc) {
}
return outputBuffer.toString();
}
/**
* Modifies the loginwindow.plist file to either include or exclude
* starting up LimeWire.
*/
public static void setLoginStatus(boolean allow) {
try {
SetLoginStatusNative(allow);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
}
/**
* Gets the full user's name.
*/
public static String getUserName() {
try {
return GetCurrentFullUserName();
} catch(UnsatisfiedLinkError ule) {
// No big deal, just return user name.
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
return CommonUtils.getUserName();
}
}
/**
* Retrieves the app directory & name.
* If the user is not running from the bundled app as we named it,
* defaults to /Applications/LimeWire/ as the directory of the app.
*/
public static String getAppDir() {
String appDir = "/Applications/LimeWire/";
String path = CommonUtils.getCurrentDirectory().getPath();
int app = path.indexOf("LimeWire.app");
if(app != -1)
appDir = path.substring(0, app);
return appDir + APP_NAME;
}
/**
* This sets LimeWire as the default handler for this file type.
* @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
*/
public static void setLimewireAsDefaultFileTypeHandler(String fileType) {
try {
SetDefaultFileTypeHandler(fileType, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
}
/**
* This sets LimeWire as the default handler for this URL scheme.
* @param url scheme -- the designator for the protocol, e.g. magnet
*/
public static void setLimewireAsDefaultURLSchemeHandler(String urlScheme) {
try {
SetDefaultURLSchemeHandler(urlScheme, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
}
/**
* This checks whether LimeWire is the default handler for this file type.
* @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
* @return true if LimeWire is the default handler for this file type
*/
public static boolean isLimewireDefaultFileTypeHandler(String fileType) {
try {
return IsApplicationTheDefaultFileTypeHandler(fileType, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
return true;
}
/**
* This checks whether LimeWire is the default handler for this URL scheme.
* @param urlScheme -- the designator for the protocol, e.g. magnet
* @return true if LimeWire is the default handler for this URL scheme
*/
public static boolean isLimewireDefaultURLSchemeHandler(String urlScheme) {
try {
return IsApplicationTheDefaultURLSchemeHandler(urlScheme, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
return true;
}
/**
* This checks whether any applications are registered as handlers for this fileType in the OS-X
* launch services database.
*
* @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
* @return true if any application is registered as a handler for this file type
*/
public static boolean isFileTypeHandled(String fileType) {
try {
return IsFileTypeHandled(fileType);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
return true;
}
/**
* This checks whether any applications are registered as handlers for this URL scheme in the OS-X
* launch services database.
*
* @param urlScheme -- the designator for the protocol, e.g. magnet
* @return true if any application is registered as a handler for this URL scheme
*/
public static boolean isURLSchemeHandled(String urlScheme) {
try {
return IsURLSchemeHandled(urlScheme);
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
return true;
}
/**
* This tries to change the file type handler for the given file type from LimeWire to another application.
* Basically, it just changes the default handler application to the first application in the list
* returned by launch services that isn't LimeWire. It might fail if no other handlers are registered for this file type.
* The list of handlers that are used internally in this method should not be shown to users as they are probably not understandable
* by users. For example LimeWire is represented by the application bundle identifier com.limegroup.gnutella.
*
* @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
*/
public static void tryChangingDefaultFileTypeHandler(String fileType) {
try {
String[] handlers = GetAllHandlersForFileType(fileType);
if (handlers != null) {
for (String handler : handlers) {
if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
SetDefaultFileTypeHandler(fileType, handler);
break;
}
}
}
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
}
/**
* This method returns true if there are any other applications on the user's system that have
* registered themselves as handlers for the given file type.
*
* @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
*/
public static boolean canChangeDefaultFileTypeHandler(String fileType) {
try {
String[] handlers = GetAllHandlersForFileType(fileType);
if (handlers != null) {
for (String handler : handlers) {
if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
return true;
}
}
}
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
return false;
}
/**
* This tries to change the URL scheme handler for the given file type from LimeWire to another application.
* Basically, it just changes the default handler application to the first application in the list
* returned by launch services that isn't LimeWire. It might fail if no other handlers are registered for this URL scheme.
* The list of handlers that are used internally in this method should not be shown to users as they are probably not understandable
* by users. For example LimeWire is represented by the application bundle identifier com.limegroup.gnutella.
*
* @param urlScheme -- the designator for the protocol, e.g. magnet
*/
public static void tryChangingDefaultURLSchemeHandler(String urlScheme) {
try {
String[] handlers = GetAllHandlersForURLScheme(urlScheme);
if (handlers != null) {
for (String handler : handlers) {
if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
SetDefaultURLSchemeHandler(urlScheme, handler);
break;
}
}
}
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
}
/**
* This method returns true if there are any other applications on the user's system that have
* registered themselves as handlers for the given URL scheme.
*
* @param urlScheme -- the designator for the protocol, e.g. magnet
*/
public static boolean canChangeDefaultURLSchemeHandler(String urlScheme) {
try {
String[] handlers = GetAllHandlersForURLScheme(urlScheme);
if (handlers != null) {
for (String handler : handlers) {
if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
return true;
}
}
}
} catch(UnsatisfiedLinkError ule) {
LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
}
return false;
}
/**
* This method opens up a native OS-X file dialog. These native dialogs are better than the FileDialog and JFileChooser
* currently available in jdk6, because they have the native look and feel and navigation features of a FileDialog, but
* they also allow for multiple file selections as JFileChoosers do. If a native file dialog cannot be opened
* however because the native library cannot be found, then this shows a Java based dialog instead.
*
* @param dialogTitle - the title to be shown in the dialog. this should already have been translated.
* @param directory - the directory that the file dialog should open to
* @param canChooseFiles - whether files can be selected
* @param canChooseDirectories - whether directories can be selected
* @param allowMultipleSelections - whether multiple files or directories can be selected
* @return an array of file objects that were selected by the user or null if the user canceled the operation
*/
public static List<File> openNativeFileDialog(final String dialogTitle, final File directory, final boolean canChooseFiles,
final boolean canChooseDirectories, final boolean allowMultipleSelections,
final FileFilter filter) {
try {
final String directoryAbsolutePath = (directory != null) ? directory.getAbsolutePath() : null;
String[] filePaths = (String[]) Worker.post(new Job()
{
@Override
public Object run()
{
String[] filePaths = OpenNativeFileDialog(dialogTitle, directoryAbsolutePath, canChooseFiles, canChooseDirectories, allowMultipleSelections);
return filePaths;
}
});
if (filePaths == null) {
return null;
} else {
List<File> selectedFileList = new ArrayList<File>();
for (String filePath : filePaths) {
File selectedFile = new File(filePath);
// since we couldn't pass the file filter over to the native dialog,
// let's filter the files here if the filter is not null...
if (filter != null) {
if (filter.accept(selectedFile)) {
selectedFileList.add(selectedFile);
}
} else {
selectedFileList.add(selectedFile);
}
}
return selectedFileList;
}
} catch(UnsatisfiedLinkError ule) {
// If we can't open up a native file dialog, then let's open a Java dialog in the same way we did before we
// started using native dialogs
return openJavaFileDialog(dialogTitle, directory, canChooseFiles, canChooseDirectories, allowMultipleSelections, filter);
}
}
/**
* This opens up a Java file dialog that's been fine tuned for OS-X for selecting files or directories.
* Java based file dialogs (as of JDK6) do not allow users to select multiple files or directories.
* Clients should prefer to use the openNativeFileDialog() method rather than this one,
* and for this reason this method is currently private.
* This method is intended to serve only as a fallback if the native library for opening
* native file dialogs cannot be loaded.
*
* @param dialogTitle - the title to be shown in the dialog. this should already have been translated.
* @param directory - the directory that the file dialog should open to
* @param canChooseFiles - whether files can be selected
* @param canChooseDirectories - whether directories can be selected
* @param allowMultipleSelections - whether multiple files or directories can be selected. (This is ignored
* when using a Java based dialog.)
* @param filter - a file filter for disallowing users to select certain files
* @return an array of file objects that were selected by the user or null if the user canceled the operation
*/
private static List<File> openJavaFileDialog(String dialogTitle, File directory, boolean canChooseFiles,
boolean canChooseDirectories, boolean allowMultipleSelections,
final FileFilter filter) {
FileDialog dialog;
if(canChooseDirectories && !canChooseFiles) {
dialog = MacUtils.getFolderDialog(null);
} else {
dialog = new FileDialog(new LimeJFrame(), "");
}
dialog.setTitle(dialogTitle);
if(filter != null) {
FilenameFilter f = new FilenameFilter() {
public boolean accept(File dir, String name) {
return filter.accept(new File(dir, name));
}
};
dialog.setFilenameFilter(f);
}
dialog.setVisible(true);
String dirStr = dialog.getDirectory();
String fileStr = dialog.getFile();
if((dirStr==null) || (fileStr==null))
return null;
// if the filter didn't work, pretend that the person picked
// nothing
File f = new File(dirStr, fileStr);
if(filter != null && !filter.accept(f))
return null;
return Collections.singletonList(f);
}
/**
* Uses OS-X's launch services API to check whether any application has registered itself
* as a handler for this file type.
*/
private static final native boolean IsFileTypeHandled(String fileType);
/**
* Uses OS-X's launch services API to check whether any application has registered itself
* as a handler for this URL scheme.
*/
private static final native boolean IsURLSchemeHandled(String urlScheme);
/**
* Uses OS-X's launch services API to check whether the given application is the default handler for this
* file type.
*/
private static final native boolean IsApplicationTheDefaultFileTypeHandler(String fileType, String applicationBundleIdentifier);
/**
* Uses OS-X's launch services API to check whether the given application is the default handler for this
* URL scheme.
*/
private static final native boolean IsApplicationTheDefaultURLSchemeHandler(String urlScheme, String applicationBundleIdentifier);
/**
* Uses OS-X's launch services API to set the given application as the default handler for this file type.
*/
private static final native int SetDefaultFileTypeHandler(String fileType, String applicationBundleIdentifier);
/**
* Uses OS-X's launch services API to set the given application as the default handler for this URL scheme.
*/
private static final native int SetDefaultURLSchemeHandler(String urlScheme, String applicationBundleIdentifier);
/**
* Uses OS-X's launch services API to get all the handlers for this file type.
*/
private static final native String[] GetAllHandlersForFileType(String fileType);
/**
* Uses OS-X's launch services API to get all the handlers for this URL scheme.
*/
private static final native String[] GetAllHandlersForURLScheme(String ulrScheme);
/**
* Open a native file dialog for selecting files and folders.
* Native dialogs have the advantage of preserving the look and feel of
* the platform while still allowing for multiple file selections.
*/
private static final native String[] OpenNativeFileDialog(String title, String directoryPath, boolean canChooseFiles,
boolean canChooseDirectories, boolean allowMultipleSelections);
/**
* Gets the full user's name.
*/
private static final native String GetCurrentFullUserName();
/**
* [Un]registers LimeWire from the startup items list.
*/
private static final native void SetLoginStatusNative(boolean allow);
}