[banshee] OSX: Add hardware support for USB mass storage devices (bgo#682087)
- From: Bertrand Lorentz <blorentz src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [banshee] OSX: Add hardware support for USB mass storage devices (bgo#682087)
- Date: Sun, 19 Aug 2012 20:26:59 +0000 (UTC)
commit 9b5f892fd83019179d6f8de1e905ae2551afc8cf
Author: Timo DÃrr <timo latecrew de>
Date: Sun Aug 19 16:01:10 2012 +0200
OSX: Add hardware support for USB mass storage devices (bgo#682087)
This commit adds an implementation of an OSX-specific HardwareManager.
It supports USB mass storage devices, which can then be used through
Banshee.Dap. This enables device/playlists syncing over USB, like i.e.
Android devices, or other USB mass storage devices with an
.is_audio_player file present.
Signed-off-by: Bertrand Lorentz <bertrand lorentz gmail com>
.../Banshee.Hardware.Osx/CdromDevice.cs | 108 ++++++
.../Banshee.Osx/Banshee.Hardware.Osx/Device.cs | 167 +++++++++
.../Banshee.Osx/Banshee.Hardware.Osx/DiscVolume.cs | 78 +++++
.../LowLevel/CoreFoundation.cs | 74 ++++
.../LowLevel/DiskArbitration.cs | 74 ++++
.../Banshee.Hardware.Osx/LowLevel/IOKit.cs | 107 ++++++
.../LowLevel/OsxDiskArbiter.cs | 354 ++++++++++++++++++++
.../Banshee.Hardware.Osx/LowLevel/OsxUsbData.cs | 107 ++++++
.../Banshee.Osx/Banshee.Hardware.Osx/UsbDevice.cs | 70 ++++
.../Banshee.Osx/Banshee.Hardware.Osx/UsbVolume.cs | 101 ++++++
.../Banshee.Osx/Banshee.Hardware.Osx/Volume.cs | 179 ++++++++++
src/Backends/Banshee.Osx/Banshee.Osx.addin.xml | 2 +-
src/Backends/Banshee.Osx/Banshee.Osx.csproj | 16 +-
.../Banshee.OsxBackend/HardwareManager.cs | 140 +++++++-
.../Banshee.Osx/Banshee.OsxBackend/OsxService.cs | 25 ++-
src/Backends/Banshee.Osx/Makefile.am | 11 +
16 files changed, 1597 insertions(+), 16 deletions(-)
---
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/CdromDevice.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/CdromDevice.cs
new file mode 100644
index 0000000..8b91f48
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/CdromDevice.cs
@@ -0,0 +1,108 @@
+//
+// CdromDevice.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using MonoMac.Foundation;
+
+using Banshee.Hardware;
+using Banshee.Hardware.Osx.LowLevel;
+
+namespace Banshee.Hardware.Osx
+{
+ public class CdromDevice : BlockDevice, ICdromDevice
+ {
+ public CdromDevice (DeviceArguments arguments) : base (arguments)
+ {
+ }
+
+ #region ICdromDevice implementation
+ public bool LockDoor ()
+ {
+ return true;
+ }
+
+ public bool UnlockDoor ()
+ {
+ return true;
+ }
+
+ public bool IsDoorLocked {
+ get {
+ return false;
+ }
+ }
+ #endregion
+ }
+ public class BlockDevice : Device, IBlockDevice
+ {
+ private IVolume v;
+
+ public BlockDevice (DeviceArguments arguments) : base (arguments)
+ {
+ this.v = new DiscVolume (arguments, this);
+ }
+
+ #region IEnumerable implementation
+ IEnumerator<IVolume> IEnumerable<IVolume>.GetEnumerator ()
+ {
+ yield return v;
+ }
+ #endregion
+
+ #region IBlockDevice implementation
+ public string DeviceNode {
+ get {
+ return "/dev/disk3";
+ }
+ }
+
+ public IEnumerable<IVolume> Volumes {
+ get {
+ List<IVolume> l = new List<IVolume> ();
+ l.Add (v);
+ return l;
+ }
+ }
+
+ public bool IsRemovable {
+ get {
+ return true;
+ }
+ }
+ #endregion
+
+ #region IEnumerable implementation
+ public IEnumerator GetEnumerator ()
+ {
+ throw new System.NotImplementedException ();
+ }
+ #endregion
+ }
+}
+
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/Device.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/Device.cs
new file mode 100644
index 0000000..b304612
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/Device.cs
@@ -0,0 +1,167 @@
+//
+// Device.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Security.Cryptography;
+
+using MonoMac.Foundation;
+
+using Banshee.Hardware;
+using Banshee.Hardware.Osx.LowLevel;
+
+namespace Banshee.Hardware.Osx
+{
+ public class Device : IDevice, IComparable, IDisposable
+ {
+ // this is a low-level NSDictionary the OS X DiskArbitration framework
+ // gives us back for any disk devices or volumes and holds ALL information
+ // we need for a given device
+ protected DeviceArguments deviceArguments;
+
+ public Device (DeviceArguments arguments)
+ {
+ this.deviceArguments = arguments;
+
+ // copy values from the NSDictionary so we don't rely on it later
+ this.vendor = deviceArguments.DeviceProperties.GetStringValue("DADeviceVendor");
+ this.uuid = GetUUIDFromProperties (deviceArguments.DeviceProperties);
+
+ this.name = deviceArguments.DeviceProperties.GetStringValue ("DAVolumeName");
+ if (string.IsNullOrEmpty (this.name)) {
+ this.name = deviceArguments.DeviceProperties.GetStringValue ("DAMediaName");
+ }
+
+ this.product = deviceArguments.DeviceProperties.GetStringValue("DADeviceModel");
+ }
+
+ #region IDevice implementation
+ public IUsbDevice ResolveRootUsbDevice ()
+ {
+ // TODO this should be refactored - devices don't need to be usb devices
+ // There's also firewire, thunderbolt, etc.
+ if ((this as IUsbDevice) != null)
+ return (IUsbDevice) this;
+ else {
+ // return a fake usb device
+ return new UsbDevice (deviceArguments) as IUsbDevice;
+ }
+ }
+
+ public IUsbPortInfo ResolveUsbPortInfo ()
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Dumps the device details. Mainly usefull for debugging.
+ /// </summary>
+ public void DumpDeviceDetails ()
+ {
+ foreach (var key in deviceArguments.DeviceProperties.Keys)
+ Console.WriteLine ("{0} => {1}",
+ key.ToString (),
+ deviceArguments.DeviceProperties.GetStringValue (key.ToString ())
+ );
+ }
+
+ public void Dispose ()
+ {}
+
+ protected static string GetUUIDFromProperties (NSDictionary properties)
+ {
+ // this is somewhat troublesome
+ // some devices have a filesystem UUID (i.e. HFS+ formated ones), but most other devices don't.
+ // As the different devices/volumes have not really always a key in common, we use different keys
+ // depending on the device type, and generate a UUID conforming 16byte value out of it
+
+ string uuid_src =
+ properties.GetStringValue ("DAMediaBSDName") ??
+ properties.GetStringValue ("DADevicePath") ??
+ properties.GetStringValue ("DAVolumePath");
+
+ if (string.IsNullOrEmpty (uuid_src)) {
+ Hyena.Log.ErrorFormat ("Tried to create a device for which we can't determine a UUID");
+ throw new ApplicationException ("Could not determine a UUID for the device");
+ }
+
+ // TODO actually transform into a real UUID
+ return uuid_src;
+ }
+
+ protected string uuid;
+ public string Uuid {
+ get {
+ return uuid;
+ }
+ }
+
+ protected string serial;
+ public string Serial {
+ get {
+ return "123456789";
+ }
+ }
+
+ protected string name;
+ public string Name {
+ get {
+ return name;
+ }
+ }
+
+ protected string product;
+ public string Product {
+ get {
+ return product;
+ }
+ }
+
+ protected string vendor;
+ public string Vendor {
+ get {
+ return vendor;
+ }
+ }
+
+ public IDeviceMediaCapabilities MediaCapabilities {
+ get {
+ return null;
+ }
+ }
+ #endregion
+
+ #region IComparable implementation
+ public int CompareTo (object device)
+ {
+ if (device is IDevice) {
+ return this.Uuid.CompareTo (((IDevice) device).Uuid);
+ } else {
+ throw new ArgumentException ("object is not an IDevice");
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/DiscVolume.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/DiscVolume.cs
new file mode 100644
index 0000000..08b136f
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/DiscVolume.cs
@@ -0,0 +1,78 @@
+//
+// DiscVolume.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using MonoMac.Foundation;
+using Banshee.Hardware.Osx.LowLevel;
+
+
+namespace Banshee.Hardware.Osx
+{
+
+ public class DiscVolume : Volume, IDiscVolume
+ {
+ public DiscVolume (DeviceArguments arguments, IBlockDevice b) : base(arguments, b)
+ {
+ }
+ #region IDiscVolume implementation
+ public bool HasAudio {
+ get {
+ return true;
+ }
+ }
+
+ public bool HasData {
+ get {
+ return false;
+ }
+ }
+
+ public bool HasVideo {
+ get {
+ return false;
+ }
+ }
+
+ public bool IsRewritable {
+ get {
+ return false;
+ }
+ }
+
+ public bool IsBlank {
+ get {
+ return false;
+ }
+ }
+
+ public ulong MediaCapacity {
+ get {
+ return 128338384858;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/CoreFoundation.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/CoreFoundation.cs
new file mode 100644
index 0000000..6c048ca
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/CoreFoundation.cs
@@ -0,0 +1,74 @@
+//
+// CoreFoundation.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright (C) 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using MonoMac;
+using MonoMac.CoreFoundation;
+using MonoMac.Foundation;
+using MonoMac.AppKit;
+using MonoMac.ObjCRuntime;
+
+namespace Banshee.Hardware.Osx.LowLevel
+{
+ // Missing pieces that are not present in MonoMac.CoreFoundation
+ internal class CoreFoundation
+ {
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern IntPtr CFAllocatorGetDefault ();
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern IntPtr CFRunLoopGetCurrent ();
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern IntPtr CFRunLoopCopyCurrentMode (IntPtr runloop);
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern void CFRunLoopRun ();
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern void CFRunLoopStop (IntPtr runloop);
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern IntPtr CFURLCopyFileSystemPath (IntPtr url, uint style);
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern void CFRelease (IntPtr ptr);
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern bool CFNumberGetValue (IntPtr number, int numberType, out Int32 val);
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern void CFShow (IntPtr obj);
+
+ [DllImport (MonoMac.Constants.CoreFoundationLibrary)]
+ public static extern IntPtr CFStringCreateWithCString (IntPtr number, string str, int encoding);
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/DiskArbitration.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/DiskArbitration.cs
new file mode 100644
index 0000000..1df7549
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/DiskArbitration.cs
@@ -0,0 +1,74 @@
+//
+// DiskArbitration.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace Banshee.Hardware.Osx.LowLevel
+{
+ public static class DiskArbitration
+ {
+ private const string DiskArbitrationLibrary = "/Systems/Library/Frameworks/DiskArbitration.framework/DiskArbitration";
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DASessionCreate (IntPtr allocator);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DARegisterDiskAppearedCallback (IntPtr session, IntPtr match, IntPtr callback, IntPtr context);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DARegisterDiskDescriptionChangedCallback (IntPtr session, IntPtr match, IntPtr watch, IntPtr callback, IntPtr context);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DARegisterDiskDisappearedCallback (IntPtr session, IntPtr match, IntPtr callback, IntPtr context);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DAUnregisterCallback (IntPtr session, IntPtr callback, IntPtr context);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DASessionScheduleWithRunLoop (IntPtr session , IntPtr runLoop , IntPtr runloopMode);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DASessionUnscheduleFromRunLoop (IntPtr session , IntPtr runLoop , IntPtr runloopMode);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DADiskCopyDescription (IntPtr disk);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DADiskCopyIOMedia (IntPtr disk);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern void DADiskUnmount (IntPtr disk, int unmountOptions, UnmountCallback callback, IntPtr context);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DADiskCreateFromBSDName (IntPtr allocator, IntPtr da_session_ref, string name);
+
+ [DllImport (DiskArbitrationLibrary)]
+ public static extern IntPtr DADiskCreateFromVolumePath (IntPtr allocator, IntPtr da_session_ref, IntPtr urlref);
+
+ public delegate void UnmountCallback (IntPtr disk, IntPtr dissenter, IntPtr context);
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/IOKit.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/IOKit.cs
new file mode 100644
index 0000000..2584a15
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/IOKit.cs
@@ -0,0 +1,107 @@
+//
+// IOKit.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright (C) 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using MonoMac;
+using MonoMac.CoreFoundation;
+using MonoMac.Foundation;
+using MonoMac.AppKit;
+using MonoMac.ObjCRuntime;
+
+namespace Banshee.Hardware.Osx.LowLevel
+{
+ /// <summary>
+ /// Wrapper against the OS X IOKit framework.
+ /// Especially helpful for is the "IORegistryExplorer" program that ships with Xcode to browse
+ /// connected devices and review their properties.
+ /// </summary>
+ internal class IOKit
+ {
+ private const string IOKitLibrary = "/SystemS/Library/Frameworks/IOKit.framework/IOKit";
+
+ public static IntPtr FindInParent (IntPtr entry , CFString field)
+ {
+ // the field we search for, i.e. idVendor as the usb vendor id
+ IntPtr key = field.Handle;
+
+ IntPtr ptr = IORegistryEntryCreateCFProperty (entry, key, IntPtr.Zero, 0);
+ if (ptr == IntPtr.Zero) {
+ // key does not exist, go up one level
+ IntPtr parent;
+
+ // we search in the IOService plane - other planes might be IOUSB or IODeviceTree etc.
+ // see IORegistryExplorer program that ships with OS X Xcode.
+ IORegistryEntryGetParentEntry (entry , "IOService", out parent);
+ if (parent != IntPtr.Zero) {
+ return FindInParent (parent, field);
+ } else {
+ return IntPtr.Zero;
+ }
+ } else {
+ return entry;
+ }
+ }
+
+ public static IntPtr GetUsbProperty (IntPtr registry_entry, CFString key)
+ {
+ if (registry_entry == IntPtr.Zero || key.Handle == IntPtr.Zero) {
+ return IntPtr.Zero;
+ }
+
+ IntPtr parent_entry = IOKit.FindInParent (registry_entry, key);
+ if (parent_entry == IntPtr.Zero) {
+ return IntPtr.Zero;
+ }
+
+ IntPtr ptr = IORegistryEntryCreateCFProperty (parent_entry, key.Handle, IntPtr.Zero, 0);
+ //CFShow (ptr);
+ return ptr;
+ }
+
+ [DllImport (IOKitLibrary)]
+ public static extern void IOObjectRelease (IntPtr obj);
+
+ [DllImport (IOKitLibrary)]
+ public static extern IntPtr IORegistryEntryCreateCFProperty (IntPtr entry, IntPtr key, IntPtr allocator, uint options);
+
+ [DllImport (IOKitLibrary)]
+ public static extern void IORegistryEntryGetParentIterator (IntPtr iterator, IntPtr plane, out IntPtr parent);
+
+ [DllImport (IOKitLibrary)]
+ public static extern void IORegistryEntryGetParentEntry (IntPtr entry, string plane, out IntPtr parent);
+
+ [DllImport (IOKitLibrary)]
+ public static extern IntPtr IORegistryEntryFromPath (IntPtr master_port, string path);
+
+ [DllImport (IOKitLibrary)]
+ public static extern IntPtr IORegistryEntryGetPath (IntPtr entry, string plane, string path);
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/OsxDiskArbiter.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/OsxDiskArbiter.cs
new file mode 100644
index 0000000..ddcd25a
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/OsxDiskArbiter.cs
@@ -0,0 +1,354 @@
+//
+// OsxDiskArbiter.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright (C) 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using MonoMac;
+using MonoMac.CoreFoundation;
+using MonoMac.Foundation;
+using MonoMac.AppKit;
+using MonoMac.ObjCRuntime;
+
+namespace Banshee.Hardware.Osx.LowLevel
+{
+ public delegate void DiskAppearedHandler (object o, DeviceArguments args);
+ public delegate void DiskDisappearedHandler (object o, DeviceArguments args);
+ public delegate void DiskDescriptionChangedHandler (object o, DeviceArguments args);
+
+ public class DeviceArguments
+ {
+ public DeviceArguments (NSDictionary properties)
+ {
+ DeviceProperties = properties;
+ }
+
+ public DeviceArguments (NSDictionary properties, OsxDiskArbiter arbiter) : this (properties)
+ {
+ DiskArbiter = arbiter;
+ }
+
+ public DeviceArguments (NSDictionary properties, OsxUsbData usbdata) : this (properties)
+ {
+ UsbInfo = usbdata;
+ }
+
+ public NSDictionary DeviceProperties;
+ public OsxUsbData UsbInfo;
+ public OsxDiskArbiter DiskArbiter;
+ }
+
+ /// <summary>
+ /// Wrapper against the OS X DiskArbitation framework. Some very useful links for a better understanding:
+ /// <see href="http://www.thoughtstuff.com/rme/weblog/?p=3">This blog</see>,
+ /// <see href="http://www.cocoaintheshell.com/2011/03/dadiskmountapprovalcallback-double-callback/">this</see>,
+ /// <see href="http://joubert.posterous.com/notification-when-usb-storage-device-is-conne">this</see> as well as
+ /// <see href="http://developer.apple.com/library/mac/#documentation/Darwin/Reference/DiscArbitrationFramework/DiskArbitration_h/index.html">
+ /// Apples DiskArbitation framework documentation</see>,
+ /// <see href="http://developer.apple.com/library/mac/#samplecode/USBPrivateDataSample/Introduction/Intro.html">This C sample</see>,
+ /// and your local copy of /System/Library/Framework/DiskArbitration/DiskArbitration.h.
+ /// Especially helpful for is the "IORegistryExplorer" program that ships with Xcode.
+ /// </summary>
+ public class OsxDiskArbiter : IDisposable
+ {
+ public NSAutoreleasePool pool;
+
+ public OsxDiskArbiter ()
+ {
+ pool = new NSAutoreleasePool ();
+ }
+
+ /// <summary>
+ /// Called when a disk/volume "appears" to the system. This can be an USB Drive plugged in, as well as network volumes, FUSE filesystems etc.
+ /// Note that this callback is triggered for the main disk (i.e. The USB Drive itself) as well as all Volumes on it (i.e. all partitions on an USB
+ /// drive).
+ /// </summary>
+ /// <remarks>This function should not be used to retrieve the mountpoint of a newly plugged disk/volume.
+ /// It is totally indeterministic whether at the time the callback fires the disk already is mounted
+ /// in the system, and the DAVolumePath field may be null. Use <see cref='DiskDescriptionChanged'/>
+ /// instead.
+ /// </remarks>
+ public event DiskAppearedHandler DeviceAppeared;
+
+ /// <summary>
+ /// Occurs when device disappeared, i.e. is disconnected or unmounted.
+ /// </summary>
+ public event DiskDisappearedHandler DeviceDisappeared;
+
+ /// <summary>
+ /// Occurs when device description changed. This event should be used to watch for newly added usb sticks/drives, as it will have the DAVolumePath
+ /// (=mountpoint) in it.
+ /// </summary>
+ public event DiskDescriptionChangedHandler DeviceChanged;
+
+ private delegate void diskAppearedCallback (IntPtr diskRef, IntPtr context);
+ private delegate void diskDisappearedCallback (IntPtr diskRef, IntPtr context);
+ private delegate void diskChangedCallback (IntPtr diskRef, IntPtr keys, IntPtr context);
+
+ private Thread listenThread;
+ private IntPtr da_session;
+ private IntPtr runloop;
+
+ private IntPtr callback_appeared;
+ private IntPtr callback_disappeared;
+ private IntPtr callback_changed;
+
+ public void StartListening ()
+ {
+ listenThread = new Thread( () => {
+ using (var arp = new NSAutoreleasePool ()) {
+ startArbiter ();
+ }
+ });
+ listenThread.Start ();
+ }
+
+ /// <summary>
+ /// Called when a disk/volume "appears" to the system. This can be an USB Drive plugged in, as well as network volumes, FUSE filesystems etc.
+ /// Note that this callback is triggered for the main disk (i.e. The USB Drive itself) as well as all Volumes on it (i.e. all partitions on an USB
+ /// drive).
+ /// </summary>
+ ///
+ /// <param name='disk'>
+ /// A reference of type DADiskRef
+ /// </param>
+ /// <param name='context'>
+ /// Application-specific context. Currently not in use.
+ /// </param>
+ ///
+ /// <remarks>This function should not be used to retrieve the mountpoint of a newly plugged disk/volume.
+ /// It is totally indeterministic whether at the time the /// callback fires the disk already is mounted
+ /// in the system, and the DAVolumePath field may be null. Use <see cref='DiskDescriptionChanged'/>
+ /// instead.
+ /// </remarks>
+ private void NativeDiskAppeared (IntPtr disk, IntPtr context)
+ {
+ if (this.DeviceAppeared == null) {
+ // if no-one subscribed to this event, do nothing
+ return;
+ }
+
+ IntPtr device = DiskArbitration.DADiskCopyIOMedia (disk);
+ IntPtr propertiesRef = DiskArbitration.DADiskCopyDescription (disk);
+
+ // using MonoMac we can get a managed NSDictionary from the pointer
+ NSDictionary properties = new NSDictionary (propertiesRef);
+ DeviceArguments deviceArguments = new DeviceArguments (properties, this);
+
+ // get usb data
+ if (properties.HasKey ("DADeviceProtocol") && properties.GetStringValue ("DADeviceProtocol") == "USB") {
+ OsxUsbData usb = new OsxUsbData (device);
+ deviceArguments.UsbInfo = usb;
+ }
+ IOKit.IOObjectRelease (device);
+
+ // trigger the public event for any subscribers
+ this.DeviceAppeared (this, deviceArguments);
+
+ GC.KeepAlive (this);
+ }
+
+ private void NativeDiskChanged (IntPtr disk, IntPtr keys, IntPtr context)
+ {
+ if (this.DeviceChanged == null) {
+ // if no-one subscribed to this event, do nothing
+ return;
+ }
+
+ IntPtr device = DiskArbitration.DADiskCopyIOMedia (disk);
+ IntPtr propertiesRef = DiskArbitration.DADiskCopyDescription (disk);
+
+ // using MonoMac we can get a managed NSDictionary from the pointer
+ NSDictionary properties = new NSDictionary (propertiesRef);
+ DeviceArguments deviceArguments = new DeviceArguments (properties, this);
+
+ if (properties.HasKey ("DADeviceProtocol") && properties.GetStringValue ("DADeviceProtocol") == "USB") {
+ OsxUsbData usb = new OsxUsbData (device);
+ deviceArguments.UsbInfo = usb;
+ }
+
+ IOKit.IOObjectRelease (device);
+
+ // trigger the public event for any subscribers
+ this.DeviceChanged (this, deviceArguments);
+ GC.KeepAlive (this);
+ }
+
+ private void NativeDiskDisappeared (IntPtr disk, IntPtr context)
+ {
+ if (this.DeviceDisappeared == null) {
+ // if no-one subscribed to this event, do nothing
+ return;
+ }
+
+ IntPtr device = DiskArbitration.DADiskCopyIOMedia (disk);
+ IntPtr propertiesRef = DiskArbitration.DADiskCopyDescription (disk);
+
+ NSDictionary properties = new NSDictionary (propertiesRef);
+
+ DeviceArguments deviceArguments = new DeviceArguments (properties, this);
+
+ if (properties.HasKey ("DADeviceProtocol") && properties.GetStringValue ("DADeviceProtocol") == "USB") {
+ OsxUsbData usb = new OsxUsbData (device);
+ deviceArguments.UsbInfo = usb;
+ }
+
+ IOKit.IOObjectRelease (device);
+
+ this.DeviceDisappeared (this, deviceArguments);
+ GC.KeepAlive (this);
+ }
+
+ private void startArbiter ()
+ {
+ diskAppearedCallback disk_appeared_callback = new diskAppearedCallback (NativeDiskAppeared);
+ diskChangedCallback disk_changed_callback = new diskChangedCallback (NativeDiskChanged);
+ diskDisappearedCallback disk_disappeared_callback = new diskDisappearedCallback (NativeDiskDisappeared);
+
+ // create a DiskArbitration session
+ IntPtr allocator = CoreFoundation.CFAllocatorGetDefault ();
+ da_session = DiskArbitration.DASessionCreate (allocator);
+
+ this.callback_appeared = Marshal.GetFunctionPointerForDelegate (disk_appeared_callback);
+ this.callback_changed = Marshal.GetFunctionPointerForDelegate (disk_changed_callback);
+ this.callback_disappeared = Marshal.GetFunctionPointerForDelegate (disk_disappeared_callback);
+
+ DiskArbitration.DARegisterDiskAppearedCallback (da_session, IntPtr.Zero, callback_appeared, IntPtr.Zero);
+ DiskArbitration.DARegisterDiskDescriptionChangedCallback (da_session, IntPtr.Zero, IntPtr.Zero, callback_changed, IntPtr.Zero);
+ DiskArbitration.DARegisterDiskDisappearedCallback (da_session, IntPtr.Zero, callback_disappeared, IntPtr.Zero);
+
+ //IntPtr runloop = CFRunLoopGetCurrent ();
+ runloop = MonoMac.CoreFoundation.CFRunLoop.Current.Handle;
+
+ var mode = MonoMac.CoreFoundation.CFRunLoop.CFDefaultRunLoopMode.Handle;
+ DiskArbitration.DASessionScheduleWithRunLoop (da_session, runloop, mode);
+
+ // this blocks the thread
+ CoreFoundation.CFRunLoopRun ();
+
+ // this code is actually never run, but keeps our native references
+ // and callbacks alive to prevent the GC from removing it
+ GC.KeepAlive (allocator);
+ GC.KeepAlive (da_session);
+ GC.KeepAlive (callback_changed);
+ GC.KeepAlive (callback_appeared);
+ GC.KeepAlive (callback_disappeared);
+ GC.KeepAlive (disk_appeared_callback);
+ GC.KeepAlive (disk_changed_callback);
+ GC.KeepAlive (disk_disappeared_callback);
+ }
+
+ public void Dispose ()
+ {
+ // unregister our callbacks
+ DiskArbitration.DAUnregisterCallback (da_session, callback_appeared, IntPtr.Zero);
+ DiskArbitration.DAUnregisterCallback (da_session, callback_changed, IntPtr.Zero);
+ DiskArbitration.DAUnregisterCallback (da_session, callback_disappeared, IntPtr.Zero);
+
+ var mode = MonoMac.CoreFoundation.CFRunLoop.CFDefaultRunLoopMode.Handle;
+ DiskArbitration.DASessionUnscheduleFromRunLoop (da_session, runloop, mode);
+ CoreFoundation.CFRelease (da_session);
+
+ // stop the main run loop which blocks the thread
+ CoreFoundation.CFRunLoopStop (runloop);
+ listenThread.Join ();
+ GC.SuppressFinalize (this);
+ }
+
+ // we need to map the volumeUrl's to pathes
+ // like file://localhost/Volumes/Mountpoint -> /Volumes/MountPoint
+ public static string UrlToFileSystemPath (string url, uint mode = 0) {
+ if (url == null) {
+ throw new ArgumentException ("url cannot be null");
+ }
+
+ using (var arp = new NSAutoreleasePool ()) {
+ NSUrl nsurl = new NSUrl (url);
+ NSString path = new NSString (CoreFoundation.CFURLCopyFileSystemPath (nsurl.Handle, mode));
+ return path.ToString ();
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the disk by its BSD Name.
+ /// </summary>
+ /// <returns>
+ /// The native IntPtr (corresponds to native DADiskRef) to the disk.
+ /// </returns>
+ /// <param name='bsdName'>
+ /// BSDName. A BSDName is the name of the device in the /dev device tree. Example of a valid BSDName is 'disk5s1'
+ /// </param>
+ public static IntPtr GetDiskByBSDName (string bsdName)
+ {
+ // TODO
+ return IntPtr.Zero;
+ }
+
+ public void UnmountDisk (string volumePath)
+ {
+ CFUrl url = MonoMac.CoreFoundation.CFUrl.FromUrlString (volumePath, null);
+ IntPtr disk = DiskArbitration.DADiskCreateFromVolumePath (IntPtr.Zero, da_session, url.Handle);
+
+ if (disk == IntPtr.Zero) {
+ return;
+ }
+
+ DiskArbitration.UnmountCallback cb = (IntPtr unmounted_disk, IntPtr dissenter, IntPtr context) => {
+ Hyena.Log.DebugFormat ("successfully unmounted {0}", volumePath);
+ };
+ DiskArbitration.DADiskUnmount (disk, 0, cb, IntPtr.Zero);
+ }
+ }
+
+ /// <summary>
+ /// NS dictionary helper. Allows easier access of keys in the semi-native NSDictionary
+ /// </summary>
+ public static class NSDictionaryHelper
+ {
+ public static string GetStringValue (this NSDictionary dict, string key)
+ {
+ using (var arp = new NSAutoreleasePool ()) {
+ var searchKey = dict.Keys.Where (k => k.ToString () == key).FirstOrDefault ();
+ if (searchKey == null) {
+ return null;
+ } else {
+ return dict[searchKey].ToString ();
+ }
+ }
+ }
+
+ public static bool HasKey (this NSDictionary dict, string key)
+ {
+ using (var arp = new NSAutoreleasePool ()) {
+ var searchKey = dict.Keys.Where (k => k.ToString () == key).FirstOrDefault ();
+ return (searchKey != null) ? true : false;
+ }
+ }
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/OsxUsbData.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/OsxUsbData.cs
new file mode 100644
index 0000000..00cc1a9
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/LowLevel/OsxUsbData.cs
@@ -0,0 +1,107 @@
+//
+// OsxUsbData.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright (C) 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using MonoMac;
+using MonoMac.CoreFoundation;
+using MonoMac.Foundation;
+using MonoMac.AppKit;
+using MonoMac.ObjCRuntime;
+
+namespace Banshee.Hardware.Osx.LowLevel
+{
+ public class OsxUsbData
+ {
+ public uint VendorId;
+ public uint ProductId;
+ public string ProductName;
+ public string VendorName;
+
+ // not to be confused with the iSerialNumber
+ public string UsbSerial;
+
+ public ulong LocationID;
+
+ public OsxUsbData ()
+ {}
+
+ internal OsxUsbData (IntPtr registry_entry)
+ {
+ // 1st approach - get IODeviceTree's parent locationID, then find by location ID
+ /*string path = properties.GetStringValue ("DAMediaPath");
+ IntPtr entry = IORegistryEntryFromPath (IntPtr.Zero, path);
+ CFString s = new CFString ("locationID");
+ IntPtr plane = new CFString ("IODeviceTree").Handle;
+ IntPtr parent = IntPtr.Zero;
+ IORegistryEntryGetParentEntry (entry, "IODeviceTree", out parent);
+ if (parent != IntPtr.Zero) {
+ IntPtr ptr = IORegistryEntryCreateCFProperty (parent, s.Handle, IntPtr.Zero, 0);
+ CFShow (ptr);
+ }*/
+ // TODO recursive find
+
+ // 2nd approach - walk the tree (which one?) up until we find
+ // a idVendor - at worst, up to the root
+ IntPtr cf_ref;
+
+ // populate properties from the usb device info
+
+ cf_ref = IOKit.GetUsbProperty (registry_entry, "idVendor");
+ if (cf_ref != IntPtr.Zero) {
+ Int32 num;
+ CoreFoundation.CFNumberGetValue (cf_ref, 3, out num);
+ VendorId = (uint) num;
+ }
+
+ cf_ref = IOKit.GetUsbProperty (registry_entry, "idProduct");
+ if (cf_ref != IntPtr.Zero) {
+ Int32 num;
+ CoreFoundation.CFNumberGetValue (cf_ref, 3, out num);
+ ProductId = (uint) num;
+ }
+
+ cf_ref = IOKit.GetUsbProperty (registry_entry, "USB Vendor Name");
+ if (cf_ref != IntPtr.Zero) {
+ VendorName = new CFString (cf_ref).ToString ();
+ }
+
+ cf_ref = IOKit.GetUsbProperty (registry_entry, "USB Product Name");
+ if (cf_ref != IntPtr.Zero) {
+ ProductName = new CFString (cf_ref).ToString ();
+ }
+
+ cf_ref = IOKit.GetUsbProperty (registry_entry, "USB Serial Number");
+ if (cf_ref != IntPtr.Zero) {
+ UsbSerial = new CFString (cf_ref).ToString ();
+ }
+ }
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/UsbDevice.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/UsbDevice.cs
new file mode 100644
index 0000000..6321c91
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/UsbDevice.cs
@@ -0,0 +1,70 @@
+//
+// UsbDevice.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using MonoMac.Foundation;
+
+using Banshee.Hardware;
+using Banshee.Hardware.Osx.LowLevel;
+
+namespace Banshee.Hardware.Osx
+{
+ public class UsbDevice : Device, IUsbDevice
+ {
+ public UsbDevice (DeviceArguments arguments) : base (arguments)
+ {
+ }
+
+ #region IUsbDevice implementation
+ public int ProductId {
+ get {
+ if (deviceArguments.UsbInfo != null)
+ return (int) deviceArguments.UsbInfo.ProductId;
+ else
+ return 0;
+ }
+ }
+
+ public int VendorId {
+ get {
+ if (deviceArguments.UsbInfo != null)
+ return (int) deviceArguments.UsbInfo.VendorId;
+ else
+ return 0;
+ }
+ }
+
+ public double Speed {
+ get { return 2.0; }
+ }
+
+ public double Version {
+ get { return 2.0; }
+ }
+ #endregion
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/UsbVolume.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/UsbVolume.cs
new file mode 100644
index 0000000..12b4929
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/UsbVolume.cs
@@ -0,0 +1,101 @@
+//
+// UsbVolume.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using MonoMac.Foundation;
+
+using Banshee.Hardware;
+using Banshee.Hardware.Osx.LowLevel;
+
+namespace Banshee.Hardware.Osx
+{
+ public class UsbVolume : Volume, IUsbDevice, IBlockDevice
+ {
+ public UsbVolume (DeviceArguments arguments) : base (arguments)
+ {
+ return;
+ }
+
+ #region IUsbDevice implementation
+ public int ProductId {
+ get {
+ if (deviceArguments.UsbInfo != null)
+ return (int) deviceArguments.UsbInfo.ProductId;
+ else
+ return 0;
+ }
+ }
+
+ public int VendorId {
+ get {
+ if (deviceArguments.UsbInfo != null)
+ return (int) deviceArguments.UsbInfo.VendorId;
+ else
+ return 0;
+ }
+ }
+
+ public double Speed {
+ get { return 2.0; }
+ }
+
+ public double Version {
+ get { return 1.0; }
+ }
+ #endregion
+
+ #region IEnumerable implementation
+ public System.Collections.IEnumerator GetEnumerator ()
+ {
+ throw new System.NotImplementedException ();
+ }
+ #endregion
+
+ #region IEnumerable implementation
+ IEnumerator<IVolume> IEnumerable<IVolume>.GetEnumerator ()
+ {
+ yield return this;
+ throw new System.NotImplementedException ();
+ }
+ #endregion
+
+ #region IBlockDevice implementation
+ public IEnumerable<IVolume> Volumes {
+ get {
+ yield return this;
+ }
+ }
+
+ public bool IsRemovable {
+ get {
+ throw new System.NotImplementedException ();
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/Volume.cs b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/Volume.cs
new file mode 100644
index 0000000..c11c22b
--- /dev/null
+++ b/src/Backends/Banshee.Osx/Banshee.Hardware.Osx/Volume.cs
@@ -0,0 +1,179 @@
+//
+// Volume.cs
+//
+// Author:
+// Timo DÃrr <timo latecrew de>
+//
+// Copyright 2012 Timo DÃrr
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+
+using MonoMac.Foundation;
+
+using Banshee.Hardware.Osx.LowLevel;
+
+namespace Banshee.Hardware.Osx
+{
+ public class Volume : Device, IVolume
+ {
+ private IBlockDevice block_parent;
+ public Volume (DeviceArguments arguments):base (arguments)
+ {
+ }
+
+ public Volume (DeviceArguments arguments, IBlockDevice blockdevice) : base (arguments)
+ {
+ block_parent = blockdevice;
+ }
+
+ #region IVolume implementation
+ public void Eject ()
+ {
+ Unmount ();
+ }
+
+ public void Mount ()
+ {
+ return;
+ }
+
+ public void Unmount ()
+ {
+ string volume_url = deviceArguments.DeviceProperties.GetStringValue ("DAVolumePath");
+ if (string.IsNullOrEmpty (volume_url)) {
+ return;
+ }
+ deviceArguments.DiskArbiter.UnmountDisk (volume_url);
+
+ // remove this from the devices list
+ }
+
+ public string DeviceNode {
+ get {
+ if (deviceArguments.DeviceProperties.HasKey ("DAMediaBSDName")) {
+ return "/dev/" + deviceArguments.DeviceProperties.GetStringValue ("DAMediaBSDName");
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public string MountPoint {
+ get {
+ var volume_url = deviceArguments.DeviceProperties.GetStringValue ("DAVolumePath");
+ if (volume_url == null) {
+ Hyena.Log.Error ("Trying to access device without valid DAVolumePath, aborting!");
+ throw new Exception ();
+ }
+ var mountpoint = OsxDiskArbiter.UrlToFileSystemPath (volume_url);
+ return mountpoint;
+ }
+ }
+
+ public bool IsMounted {
+ get {
+ // when a device is unmounted, it triggers a DiskDisappear so we
+ // will never see unmounted devices
+ return true;
+ }
+ }
+
+ public bool IsReadOnly {
+ get {
+ bool isWriteable;
+ if (deviceArguments.DeviceProperties.HasKey ("DAMediaWritable") &&
+ bool.TryParse (deviceArguments.DeviceProperties.GetStringValue ("DAMediaWritable"), out isWriteable)) {
+
+ return isWriteable;
+ }
+ return false;
+ }
+ }
+
+ public ulong Capacity {
+ get {
+ ulong capacity;
+ var size = deviceArguments.DeviceProperties.GetStringValue ("DAMediaSize");
+ if (size != null && ulong.TryParse (size, out capacity)) {
+ return capacity;
+ } else {
+ // try the .NET way
+ DriveInfo info = new DriveInfo (MountPoint);
+ return (ulong)info.TotalSize;
+ }
+ }
+ }
+
+ public long Available {
+ get {
+ // this is unreliable
+ DriveInfo info = new DriveInfo (MountPoint);
+ return info.AvailableFreeSpace;
+ }
+ }
+
+ public IBlockDevice Parent {
+ get { return block_parent; }
+ }
+
+ public bool ShouldIgnore {
+ get { return false; }
+ }
+
+ public string FileSystem {
+ get {
+ string fs = deviceArguments.DeviceProperties.GetStringValue ("DAMediaContent");
+ if (!string.IsNullOrEmpty (fs)) {
+ return fs;
+ }
+
+ // Defaulting to MSDOS
+ return "MSDOS";
+ }
+ }
+
+ public bool CanEject {
+ get {
+ // for now, we can determine if it's ejectable, but no code to actually eject it
+ // so return false because pressing the eject button wont work
+ return true;
+ /*
+ bool isEjectable;
+ if (deviceProperties.HasKey ("DAMediaEjectable"))
+ if (bool.TryParse (deviceProperties.GetStringValue("DAMediaEjectable"), out isEjectable))
+ return isEjectable;
+ // default - not ejectable
+ return false;
+ */
+ }
+ }
+
+ public bool CanMount {
+ get { return false; }
+ }
+
+ public bool CanUnmount {
+ get { return true; }
+ }
+ #endregion
+ }
+}
diff --git a/src/Backends/Banshee.Osx/Banshee.Osx.addin.xml b/src/Backends/Banshee.Osx/Banshee.Osx.addin.xml
index 51129b7..6043926 100644
--- a/src/Backends/Banshee.Osx/Banshee.Osx.addin.xml
+++ b/src/Backends/Banshee.Osx/Banshee.Osx.addin.xml
@@ -18,6 +18,6 @@
</Extension>
<Extension path="/Banshee/Platform/HardwareManager">
- <HardwareManager class="Banshee.OsxBackend.HardwareManager"/>
+ <HardwareManager class="Banshee.OsxBackend.HardwareManager" id="Banshee.OsxBackend.HardwareManager" />
</Extension>
</Addin>
diff --git a/src/Backends/Banshee.Osx/Banshee.Osx.csproj b/src/Backends/Banshee.Osx/Banshee.Osx.csproj
index 4e58bf9..fb1e52c 100644
--- a/src/Backends/Banshee.Osx/Banshee.Osx.csproj
+++ b/src/Backends/Banshee.Osx/Banshee.Osx.csproj
@@ -69,11 +69,23 @@
<Reference Include="glib-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f">
<Private>False</Private>
</Reference>
+ <Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="Banshee.OsxBackend\HardwareManager.cs" />
<Compile Include="Banshee.OsxBackend\OsxService.cs" />
- <Compile Include="OsxIntegration.GtkOsxApplication\GtkOsxApplication.cs" />
+ <Compile Include="OsxIntegration.GtkOsxApplication\GtkOSxApplication.cs" />
+ <Compile Include="Banshee.Hardware.Osx\UsbDevice.cs" />
+ <Compile Include="Banshee.Hardware.Osx\CdromDevice.cs" />
+ <Compile Include="Banshee.Hardware.Osx\Device.cs" />
+ <Compile Include="Banshee.Hardware.Osx\Volume.cs" />
+ <Compile Include="Banshee.Hardware.Osx\DiscVolume.cs" />
+ <Compile Include="Banshee.Hardware.Osx\UsbVolume.cs" />
+ <Compile Include="Banshee.Hardware.Osx\LowLevel\IOKit.cs" />
+ <Compile Include="Banshee.Hardware.Osx\LowLevel\OsxDiskArbiter.cs" />
+ <Compile Include="Banshee.Hardware.Osx\LowLevel\OsxUsbData.cs" />
+ <Compile Include="Banshee.Hardware.Osx\LowLevel\DiskArbitration.cs" />
+ <Compile Include="Banshee.Hardware.Osx\LowLevel\CoreFoundation.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Banshee.Osx.addin.xml">
@@ -102,5 +114,7 @@
<ItemGroup>
<Folder Include="OsxIntegration.GtkOsxApplication\" />
<Folder Include="Resources\" />
+ <Folder Include="Banshee.Hardware.Osx\" />
+ <Folder Include="Banshee.Hardware.Osx\LowLevel\" />
</ItemGroup>
</Project>
diff --git a/src/Backends/Banshee.Osx/Banshee.OsxBackend/HardwareManager.cs b/src/Backends/Banshee.Osx/Banshee.OsxBackend/HardwareManager.cs
index 054ba57..db8871b 100644
--- a/src/Backends/Banshee.Osx/Banshee.OsxBackend/HardwareManager.cs
+++ b/src/Backends/Banshee.Osx/Banshee.OsxBackend/HardwareManager.cs
@@ -2,9 +2,9 @@
// HardwareManager.cs
//
// Author:
-// Eoin Hennessy <eoin randomrules org>
+// Timo DÃrr <timo latecrew de>
//
-// Copyright (C) 2008 Eoin Hennessy
+// Copyright (C) 2012 Timo DÃrr
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
@@ -24,49 +24,163 @@
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-//
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+
+using MonoMac.AppKit;
+using MonoMac.Foundation;
using Hyena;
using Banshee.Hardware;
+using Banshee.Hardware.Osx;
+using Banshee.Hardware.Osx.LowLevel;
+using Banshee.ServiceStack;
namespace Banshee.OsxBackend
{
- public sealed class HardwareManager : IHardwareManager
+ public sealed class HardwareManager : IHardwareManager, IService
{
public event DeviceAddedHandler DeviceAdded;
public event DeviceRemovedHandler DeviceRemoved;
- public void Dispose ()
+ private List<IDevice> devices = new List<IDevice> ();
+
+ private OsxDiskArbiter diskArbiter;
+
+ public HardwareManager ()
{
+ OsxService.GlobalInit ();
+ this.diskArbiter = new OsxDiskArbiter ();
+ diskArbiter.DeviceAppeared += DeviceAppeared;
+ diskArbiter.DeviceChanged += DeviceChanged;
+ diskArbiter.DeviceDisappeared += DeviceDisappeared;
+ diskArbiter.StartListening ();
}
- public IEnumerable<IDevice> GetAllDevices ()
+ private void DeviceAppeared (object o, DeviceArguments args)
{
- yield break;
+ Device device = new Device (args);
+
+ Hyena.Log.DebugFormat ("device appeared: {0}, path: {1}", device.Uuid,
+ args.DeviceProperties.GetStringValue ("DAVolumePath"));
+
+ lock (this) {
+ // only handle devices which have a VolumePath (=MountPoint)
+ if (!args.DeviceProperties.HasKey ("DAVolumePath")) {
+ return;
+ }
+
+ var protocol = args.DeviceProperties.GetStringValue ("DADeviceProtocol");
+
+ IDevice new_device = null;
+ if (!string.IsNullOrEmpty (protocol) && protocol == "USB") {
+ new_device = new UsbVolume (args);
+ } else {
+ new_device = new Volume (args, null);
+ }
+
+ // avoid adding a device twice - might happen since DeviceAppeared and DeviceChanged both fire
+ var old_device = devices.Where (v => { return v.Uuid == new_device.Uuid; }).FirstOrDefault ();
+ if (old_device != null) {
+ return;
+ }
+ if (new_device != null) {
+ devices.Add (new_device);
+
+ // Notify that a device was added (i.e. to refresh device list)
+ DeviceAdded (this, new DeviceAddedArgs ((IDevice) new_device));
+ }
+ }
}
- private IEnumerable<T> GetAllBlockDevices<T> () where T : IBlockDevice
+ private void DeviceChanged (object o, DeviceArguments args)
{
- yield break;
+ Device device = new Device (args);
+
+ Hyena.Log.DebugFormat ("device changed: {0}, path: {1}", device.Uuid,
+ args.DeviceProperties.GetStringValue ("DAVolumePath"));
+
+ lock (this) {
+ var old_device = devices.Where (d => d.Uuid == device.Uuid).FirstOrDefault ();
+ if (old_device != null) {
+ // a device that was currently attached has changed
+ // remove the device and immediately re-add it
+ devices.Remove (old_device);
+ DeviceRemoved (old_device, new DeviceRemovedArgs (old_device.Uuid));
+ }
+
+ // do not add device without a VolumePath (=MountPoint)
+ if (!args.DeviceProperties.HasKey ("DAVolumePath")) {
+ return;
+ }
+
+ IDevice new_device = null;
+ var protocol = args.DeviceProperties.GetStringValue ("DADeviceProtocol");
+ if (!string.IsNullOrEmpty (protocol) && protocol == "USB") {
+ new_device = new UsbVolume (args);
+ } else {
+ new_device = new Volume (args);
+ }
+ devices.Add (new_device);
+ DeviceAdded (this, new DeviceAddedArgs ((IDevice) new_device));
+ }
+ }
+
+ private void DeviceDisappeared (object o, DeviceArguments args)
+ {
+ Device device = new Device (args);
+
+ Hyena.Log.InformationFormat ("device disappeared: {0}, path: {1}", device.Uuid,
+ args.DeviceProperties.GetStringValue ("DAVolumePath"));
+
+ lock (this) {
+ var old_device = devices.Where (d => d.Uuid == device.Uuid).FirstOrDefault ();
+ if (old_device != null) {
+ devices.Remove (old_device);
+ DeviceRemoved (this, new DeviceRemovedArgs (old_device.Uuid));
+ }
+ }
+ }
+
+ public void Dispose ()
+ {
+ if (diskArbiter != null) {
+ diskArbiter.Dispose ();
+ }
+ }
+
+ public IEnumerable<IDevice> GetAllDevices ()
+ {
+ var l = devices.Where (v => { return v is Volume ; }).Select (v => v as IDevice);
+
+ return l;
}
public IEnumerable<IBlockDevice> GetAllBlockDevices ()
{
- return GetAllBlockDevices<IBlockDevice> ();
+ var l = devices.Where (v => { return v is Volume; }).Select (v => v as IBlockDevice);
+
+ return l;
}
public IEnumerable<ICdromDevice> GetAllCdromDevices ()
{
- return GetAllBlockDevices<ICdromDevice> ();
+ yield break;
}
public IEnumerable<IDiskDevice> GetAllDiskDevices ()
{
- return GetAllBlockDevices<IDiskDevice> ();
+ // cdrom / dvdrom currently not supported
+ return null;
+ }
+
+ #region IService implementation
+ public string ServiceName {
+ get { return "OS X HardwareManager"; }
}
+ #endregion
}
}
-
diff --git a/src/Backends/Banshee.Osx/Banshee.OsxBackend/OsxService.cs b/src/Backends/Banshee.Osx/Banshee.OsxBackend/OsxService.cs
index 57ae1cd..97bb32e 100644
--- a/src/Backends/Banshee.Osx/Banshee.OsxBackend/OsxService.cs
+++ b/src/Backends/Banshee.Osx/Banshee.OsxBackend/OsxService.cs
@@ -4,7 +4,9 @@
// Authors:
// Aaron Bockover <abockover novell com>
// Eoin Hennessy <eoin randomrules org>
+// Timo DÃrr <timo latecrew de>
//
+// Copyright 2012 Timo DÃrr
// Copyright 2009-2010 Novell, Inc.
// Copyright 2008 Eoin Hennessy
//
@@ -32,6 +34,7 @@ using System;
using System.Collections;
using System.IO;
using System.Reflection;
+using System.Threading;
using Gtk;
using Mono.Unix;
@@ -41,6 +44,11 @@ using Banshee.Gui;
using OsxIntegration.GtkOsxApplication;
using Hyena;
+using MonoMac.CoreFoundation;
+using MonoMac.Foundation;
+using MonoMac.AppKit;
+using MonoMac.CoreWlan;
+
namespace Banshee.OsxBackend
{
public class OsxService : IExtensionService, IDisposable
@@ -49,6 +57,20 @@ namespace Banshee.OsxBackend
private InterfaceActionService interface_action_service;
private string accel_map_filename = "osx_accel_map";
+ private static bool is_nsapplication_initialized = false;
+ public static void GlobalInit ()
+ {
+ // Nearly all MonoMac related functions require that NSApplication.Init () is called
+ // before usage, however other Addins (like HardwareManager) might launch before
+ // OsxService got started
+ lock (typeof (OsxService)) {
+ if (!is_nsapplication_initialized) {
+ NSApplication.Init ();
+ is_nsapplication_initialized = true;
+ }
+ }
+ }
+
void IExtensionService.Initialize ()
{
elements_service = ServiceManager.Get<GtkElementsService> ();
@@ -81,9 +103,10 @@ namespace Banshee.OsxBackend
return true;
}
-
private void Initialize ()
{
+ GlobalInit ();
+
// load OS X specific key mappings, possibly overriding default mappings
// set in GlobalActions or $HOME/.config/banshee-1/gtk_accel_map
string accel_map = Paths.Combine (Paths.ApplicationData, accel_map_filename);
diff --git a/src/Backends/Banshee.Osx/Makefile.am b/src/Backends/Banshee.Osx/Makefile.am
index 7ed14a1..b898ec8 100644
--- a/src/Backends/Banshee.Osx/Makefile.am
+++ b/src/Backends/Banshee.Osx/Makefile.am
@@ -5,6 +5,17 @@ LINK = $(REF_BACKEND_OSX)
INSTALL_DIR = $(BACKENDS_INSTALL_DIR)
SOURCES = \
+ Banshee.Hardware.Osx/CdromDevice.cs \
+ Banshee.Hardware.Osx/Device.cs \
+ Banshee.Hardware.Osx/DiscVolume.cs \
+ Banshee.Hardware.Osx/LowLevel/CoreFoundation.cs \
+ Banshee.Hardware.Osx/LowLevel/DiskArbitration.cs \
+ Banshee.Hardware.Osx/LowLevel/IOKit.cs \
+ Banshee.Hardware.Osx/LowLevel/OsxDiskArbiter.cs \
+ Banshee.Hardware.Osx/LowLevel/OsxUsbData.cs \
+ Banshee.Hardware.Osx/UsbDevice.cs \
+ Banshee.Hardware.Osx/UsbVolume.cs \
+ Banshee.Hardware.Osx/Volume.cs \
Banshee.OsxBackend/HardwareManager.cs \
Banshee.OsxBackend/OsxService.cs \
OsxIntegration.GtkOsxApplication/GtkOsxApplication.cs
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]