MTP array properties (AINT*/AUINT*) in WPD

MTP allows for a wide range of array-based data types ranging from an array of bytes (AINT8/AUINT8) to an array of GUIDs (AINT128/AUINT128). Check out section 3.2.1 and 3.2.2 in the MTP specification for a complete list and definition.

Unfortunately the WPD API is restricted in the kind of arrays it can expose. Specifically, the WPD API can only expose VT_UI1|VT_VECTOR arrays due to marshalling limitations. This means that the rich variety of MTP array data types is "lost in translation". To be accurate, the type information is lost, but the data is still accessible as a VT_UI1|VT_VECTOR array.

To retrieve and set MTP array data type properties, a WPD application must know the specific MTP data type before-hand. It must then use this information to make sure that the data is handled correctly. For setting an array data type property, the size of the byte array provided at the WPD layer must be an exact multiple of the size of the integral MTP data type.

Let's look at how the value for an MTP array data type property is retrieved. We'll assume that the device property specified is of MTP data type AUINT32 (array of DWORDs).

Reading an array property

 // This function returns a DWORD array which must be freed with
// CoTaskMemFree. The number of the elements in the array is given
// by *pdwNumElems
HRESULT GetAUINT32DevPropValue(IPortableDevice* pDevice, 
                               WORD wDevPropCode,
                               DWORD** ppdwArray,
                               DWORD* pdwNumElems)
{
    HRESULT hr = S_OK;

    // Get the Properties interface through the Content interface 
    CComPtr<IPortableDeviceContent> spContent;
    if (hr == S_OK)
    {
        hr = pDevice->Content(&spContent);
    }

    CComPtr<IPortableDeviceProperties> spProperties;
    if (hr == S_OK)
    {
        hr = spContent->Properties(&spProperties);
    }

    // Since this is a vendor extended property, we need to use the
    // extension GUID
    PROPERTYKEY pkeyProperty;
    pkeyProperty.fmtid = WPD_PROPERTIES_MTP_VENDOR_EXTENDED_DEVICE_PROPS;
    pkeyProperty.pid = wDevPropCode; 

    // Add this propertykey to spPropertyKeys
    CComPtr<IPortableDeviceKeyCollection> spPropertyKeys;
    if (hr == S_OK)
    {
        hr = CoCreateInstance(CLSID_PortableDeviceKeyCollection,
                              NULL,
                              CLSCTX_INPROC_SERVER,
                              IID_IPortableDeviceKeyCollection,
                              (VOID**)&spPropertyKeys);
    }

    if (hr == S_OK)
    {
        hr = spPropertyKeys->Add(pkeyProperty);
    }

    // Use the GetValues API to get the value of the property
    CComPtr<IPortableDeviceValues> spPropertyValues;
    if (hr == S_OK)
    {
        hr = spProperties->GetValues(L"DEVICE", spPropertyKeys, &spPropertyValues);
    }

    // Get value of the requested property
    BYTE* pbBuffer = NULL;
    DWORD cbBuffer = 0;
    if (hr == S_OK)
    {               
        hr = spPropertyValues->GetBufferValue(pkeyProperty, &pbBuffer, &cbBuffer);
    }
    else if (hr == S_FALSE)
    {
        // Get the actual error associated with retrieving the property
        (void) spPropertyValues->GetErrorValue(pkeyProperty, &hr);
    }

    // Cast retrieved buffer to DWORD and return that
    if (hr == S_OK)
    {
        *ppdwArray = (DWORD*) pbBuffer;
        *pdwNumElems = cbBuffer / sizeof(DWORD);
    }

    return hr;
}

We have to make use of the extended GUID to construct our vendor extended property key. The actual value is obtained by the GetValues API and is returned in an IPortableDeviceValues collection. Since this is a byte array, we can directly use the GetBufferValue method to get the array. This allocates the byte array using CoTaskMemAlloc and we simply pass this onto the caller after casting it what the caller needed.

Almost identical code can be written to retrieve MTP object properties of array data type. Simply use the object property vendor extension GUID and supply the appropriate object ID parameter to the GetValues call.

As an exercise, you can convert this sample function into a template-based one so that it can work with different MTP array data types.

 

Next let's take a look at how we'd set the same AUINT32 property used above.

Setting an array property

 // Requires a DWORD array as an input parameter along with the
// size of the array (in DWORDs, /not/ in bytes)
HRESULT SetAUINT32DevPropValue(IPortableDevice* pDevice, 
                               WORD wDevPropCode,
                               DWORD* pdwArray,
                               DWORD cdwNumElems)
{
    HRESULT hr = S_OK;

    // Get the Properties interface through the Content interface 
    CComPtr<IPortableDeviceContent> spContent;
    if (hr == S_OK)
    {
        hr = pDevice->Content(&spContent);
    }

    CComPtr<IPortableDeviceProperties> spProperties;
    if (hr == S_OK)
    {
        hr = spContent->Properties(&spProperties);
    }

    // Since this is a vendor extended property, we need to use the
    // extension GUID
    PROPERTYKEY pkeyProperty;
    pkeyProperty.fmtid = WPD_PROPERTIES_MTP_VENDOR_EXTENDED_DEVICE_PROPS;
    pkeyProperty.pid = wDevPropCode; 

    // Create an IPortableDeviceValues instance to place the new value into
    CComPtr<IPortableDeviceValues> spValues;
    if (hr == S_OK)
    {
        hr = CoCreateInstance(CLSID_PortableDeviceValues,
                              NULL,
                              CLSCTX_INPROC_SERVER,
                              IID_IPortableDeviceValues,
                              (VOID**)&spValues);
    }

    // Place the new value into the collection using SetBufferValue and casting the supplied
    // array to a BYTE array
    if (hr == S_OK)
    {
        hr = spValues->SetBufferValue(pkeyProperty, (BYTE*) pdwArray, cdwNumElems * sizeof(DWORD));
    }

    // Update the properties on the DEVICE object
    CComPtr<IPortableDeviceValues> spResults;
    if (hr == S_OK)
    {
        hr = spProperties->SetValues(L"DEVICE", spValues, &spResults);
        printf("SetValues returned hr=0x%08X\n", hr);
    }

    // Display the results of the operation
    if (SUCCEEDED(hr) && spResults != NULL)
    {
        HRESULT hrTemp = S_OK;
        HRESULT hrSetValue = S_OK;

        hrTemp = spResults->GetErrorValue(pkeyProperty, &hrSetValue);
        if (hrTemp == S_OK)
        {
            printf("\tSetting array property returned hr=0x%08X\n", hrSetValue);
            hr = hrSetValue;
        }
    }

    return hr;
}

Setting MTP array properties through WPD doesn't turn out to be that hard. We simply stuff the supplied DWORD array into a IPortableDeviceValues collection as a BYTE array using SetBufferValue. We then use the SetValues API to send the value to the driver/device. We then check the results to make sure that the value was set.

Again, it should be a trivial exercise to templatize this function to work with different MTP data types.

 

Couple of comments:

  • Since the typed arrays are always converted into plain BYTE arrays, you can technically set, say, a DWORD array value on a AUINT16 property. You can even read it back as a DWORD array value; but the value on the device will still be maintained and understood as AUINT16.
  • As an exclusion to the above loophole, you cannot set the value to an array whose size in bytes is not an integral multiple of the base MTP data type. i.e. you cannot set the value of an AUINT32 property to a BYTE array of only 3, 5, 9, etc. elements. It can only be set to BYTE arrays of sizes 4,8,12,...,N*4.
  • If you absolutely must know the array data type of the MTP property you are trying to set, send the MTP GetDevicePropDesc (or GetObjectPropDesc) command using the raw SendCommand interface and then parse the returned property description dataset (see MTP spec section 5.1.2.1 or section 5.3.2.3 - the datatype is the second WORD in the dataset).