Fun with MTP in C#
Podcast Utilities, the open source set of podcast tools I work on with my friend Derek, has been pretty stable for a while now: all the use-cases we wanted were pretty much done, and we've both been happily using it to download and sync podcasts. However, it only worked with Mass Storage devices, ie. things that looked like external hard drives when connected to your PC, and we knew at some point we would possibly have to do something with MTP devices. But like any good, lazy developers we weren't going to do it until we had to. And then I bought an HTC One S...
The One S came with Ice Cream Sandwich which was nice, and had Mass Storage support - all was well. A couple of weeks after I bought it I got the software update notification and happily downloaded it. I thought it was just a small point-release, I didn't know I was getting Jellybean until I'd installed it so it was a pleasant surprise - but once I connected it to my PC I realised that it was MTP only. Doh! Suddenly implementing MTP support became a slightly higher priority.
MTP, WPD, OMG
We looked around for how to do MTP in the .Net world and realised that we were going to have to do it ourselves. There were a couple of example libraries knocking around but they were either primitive, lacking support, or pretty dead.
Microsoft's support for MTP comes via the Windows Portable Devices (WPD) API. Accessing COM from .Net is normally fairly straightforward via COM interop, but for WPD you need to do some tweaking to fix up some marshalling problems.
The information below was gleaned from WPD Team Blog, dimeby8's blog and StackOverflow - they are great resources but they don't all have the correct instructions for how to fix the marshalling.
Step by step
Add references to the PortableDeviceTypes and PortableDeviceApi type libraries:
Once you've built your project, this generates the interop assemblies that do the magic allowing the COM APIs to be called from .Net. However, in this case, some of the marshalling information is incorrect so we have to fix that.
Copy the generated Interop.PortableDeviceApiLib.dll from obj\Debug to another folder (I created one called Interop), and disassemble it using:
ildasm Interop.PortableDeviceApiLib.dll /out:pdapi.il
Open pdapi.il in your favourite text editor and make the following changes:
Replace all instances of
GetDevices([in][out] string& marshal( lpwstr) pPnPDeviceIDs,
with
GetDevices([in][out] string[] marshal( lpwstr[]) pPnPDeviceIDs,
Then for all instances of GetDeviceFriendlyName, GetDeviceDescription and GetDeviceManufacturer we need to fix the marshalling for the unicode strings they return by changing
[in][out] uint16&
to
[in][out] uint16[] marshal([])
[Note that these are the only changes I have had to make so far, but there may be others if you are using more of the API than us]
Now rename the original interop dll and reassemble the fixed one using
ilasm pdapi.il /dll /output=Interop.PortableDeviceApiLib.dll
Make sure you use the correct version of ilasm, eg. if you are building a .net 4.0 project use the one in
C:\Windows\Microsoft.NET\Framework64\v4.0.30319
Finally, remove the original reference to PortableDeviceApiLib and add a reference to your new Interop.PortableDeviceApiLib.dll assembly.
Adding MTP support to PodcastUtilities - TDD really works
When we originally started on PodcastUtilites we knew we wanted to abstract the file system for unit-testing - most of what the tools do is moving files around and we needed to be able to test that without actually touching the real file system. And so Testing helped us Drive a Design that was not coupled to any file system - people often misunderstand TDD as just unit-testing and don't get the benefits it gives in design.
As a result we already had interfaces for file/directory info (abstractions for System.IO.FileInfo and System.IO.DirectoryInfo), file utilities (Exists, Rename, Copy, Delete), etc. and the higher level functionality (Copier, Finder, Purger, etc) just depends on those abstractions. Now all we had to do was create MTP implementations - no small task, admittedly, given the low-level COM API, but we're probably about two thirds done and other than remove a couple of uses of System.IO.Path we have not had to touch the core functionality.
It's nice when you look back at a design and realise you got it (mostly) right :)