In order to enumerate the content of connected devices such as iPhones and a digital camera connected via MTP (USB), which aren't simple file drives, I looked at the example WPD API Sample. The relevant file there is ContentEnumeration.cpp.
The code generally is:
HRESULT hr = S_OK;
CComPtr<IPortableDeviceContent> pContent;
hr = pDevice->Content(&pContent);
RecursiveEnumerate(WPD_DEVICE_OBJECT_ID, pContent);
with RecursiveEnumerate defined as:
void RecursiveEnumerate(
PCWSTR pszObjectID,
IPortableDeviceContent* pContent)
{
CComPtr<IEnumPortableDeviceObjectIDs> pEnumObjectIDs;
// Print the object identifier being used as the parent during enumeration.
printf("%ws\n",pszObjectID);
HRESULT hr = pContent->EnumObjects(0, // Flags are unused
pszObjectID, // Starting from the passed in object
NULL, // Filter is unused
&pEnumObjectIDs);
// Loop calling Next() while S_OK is being returned.
while(hr == S_OK)
{
DWORD cFetched = 0;
PWSTR szObjectIDArray[NUM_OBJECTS_TO_REQUEST] = {0};
hr = pEnumObjectIDs->Next(NUM_OBJECTS_TO_REQUEST, // Number of objects to request on each NEXT call
szObjectIDArray, // Array of PWSTR array which will be populated on each NEXT call
&cFetched); // Number of objects written to the PWSTR array
if (SUCCEEDED(hr))
{
// Traverse the results of the Next() operation and recursively enumerate
// Remember to free all returned object identifiers using CoTaskMemFree()
for (DWORD dwIndex = 0; dwIndex < cFetched; dwIndex++)
{
RecursiveEnumerate(szObjectIDArray[dwIndex],pContent);
// Free allocated PWSTRs after the recursive enumeration call has completed.
CoTaskMemFree(szObjectIDArray[dwIndex]);
szObjectIDArray[dwIndex] = NULL;
}
}
}
}
For simple connected USB file drives, this gives the right file/folder contents, but for iPhones and MTP-USB devices it doesn't. For those devices the output is
o9900
o98FF
o98FE
o98FD
etc.
Windows Explorer allows those devices to be expanded to show an "Internal Storage" or "Device Storage" pseudo-folder, and the child objects of that folder are real folders such as 202403_ and files underneath that can be manipulated. Does anyone know how the real contents can be accessed?
The following example shows how to obtain filenames, instead of object names. In addition to the pContent structure we also need to maintain the pContentProperties structure as below. Filenames are available via pContentProperties as WPD_OBJECT_NAME.
First, when opening the device and getting its Contents, also get its Content Properties:
/*
Important Note: include PortableDevice.h, PortableDeviceApi.h, atlbase.h
Also link "PortableDeviceGuids.lib" via
Project/Properties/Linker/Input/Additional Dependencies
*/
CComPtr<IPortableDevice> pIPortableDevice;
CComPtr<IEnumPortableDeviceObjectIDs> pEnumObjectIDs;
CComPtr<IPortableDeviceContent> pContent;
CComPtr<IPortableDeviceProperties> pContentProperties;
//...
HRESULT hrOpenDevice = pIPortableDevice->Open(pPnpDeviceIDs[dwIndex], pClientInformation);
if (FAILED(hrOpenDevice)) return; // or throw error
HRESULT hrContent = pIPortableDevice->Content(&pContent);
if (FAILED(hrContent)) return; // or throw error
HRESULT hrContentProperties = pContent->Properties(&pContentProperties);
if (FAILED(hrContentProperties)) return; // or throw error
...
We fetch a batch of items (in this example 10) using EnumObjects from a given source or top-level object, which in this example is s10001, followed by Next. But this only returns the 10 object names. The filenames are then obtained from pContentsProperties->GetValues and pContentsProperties->GetStringValue(WPD_OBJECT_NAME, &pwszObjectName) for the object names.
PWSTR pwszObjectName = NULL;
DWORD countFetched = 0;
PWSTR szObjectIDArray[10];
HRESULT hrEnumBatch = pContent->EnumObjects(
0, L"s10001", NULL, &pEnumObjectIDs);
if (FAILED(hrEnumBatch)) return; // or throw error
HRESULT hrContentBatch = pEnumObjectIDs->Next(
10, szObjectIDArray, &countFetched);
if (FAILED(hrContentBatch)) return; // or throw error
std::vector<std::wstring> objectNames;
std::vector<std::wstring> fileNames;
for (DWORD dwIndex = 0; dwIndex < countFetched; dwIndex++) {
std::wstring objectName = std::wstring(szObjectIDArray[dwIndex]);
objectNames.push_back(objectName);
// This is where we get filenames after the object names using Properties
// Obtain filename from object name using Properties' WPD_OBJECT_NAME
HRESULT hrPropertiesForContentObject = pContentProperties->GetValues(
szObjectIDArray[dwIndex], NULL, &pObjectProperties);
if (FAILED(hrPropertiesForContentObject)) return; // or throw error
HRESULT hrPropertiesFileName = pObjectProperties->GetStringValue(
WPD_OBJECT_NAME, &pwszObjectName);
if (FAILED(hrPropertiesFileName)) return; // or throw error
std::wstring fileName = std::wstring(pwszObjectName);
fileNames.push_back(fileName);
// Cleanup: pwszObjectName
CoTaskMemFree(pwszObjectName);
pwszObjectName = NULL;
// Cleanup: pObjectProperties
pObjectProperties = NULL;
}
// Cleanup: szObjectIDArray structures
for (DWORD dwIndex = 0; dwIndex < countFetched; dwIndex++) {
// Clear szObjectIDArray structures
CoTaskMemFree(szObjectIDArray[dwIndex]);
szObjectIDArray[dwIndex] = NULL;
}
At the end of this loop, objectNames will contain entries like o10000, o10100, o10200, while filenames will contain corresponding entries like 2025-01-01, 2025-02-01, etc., which in my case happen to be folder names.
Whether a fileName is a folder or not can be tested by whether it itself succeeds with ->Next(..).