diff options
| author | Dave Wilson <[email protected]> | 2015-03-17 19:50:07 -0700 |
|---|---|---|
| committer | Dave Wilson <[email protected]> | 2015-03-17 19:50:07 -0700 |
| commit | 97cf5197cf5b882b2c689d8dc2b555f2edf8f418 (patch) | |
| tree | 46f3701832d70b420eb0fc0eb93261f9da45db3f /wpd/WpdWudfSampleDriver/WpdObjectManagement.cpp | |
| parent | ef1905bf1e8825bb31120dfb27e0daf3154d859a (diff) | |
Initial publish
Diffstat (limited to 'wpd/WpdWudfSampleDriver/WpdObjectManagement.cpp')
| -rw-r--r-- | wpd/WpdWudfSampleDriver/WpdObjectManagement.cpp | 1160 |
1 files changed, 1160 insertions, 0 deletions
diff --git a/wpd/WpdWudfSampleDriver/WpdObjectManagement.cpp b/wpd/WpdWudfSampleDriver/WpdObjectManagement.cpp new file mode 100644 index 00000000..9c3d22b2 --- /dev/null +++ b/wpd/WpdWudfSampleDriver/WpdObjectManagement.cpp @@ -0,0 +1,1160 @@ +#include "stdafx.h" +#include "WpdObjectManagement.tmh" + +WpdObjectManagement::WpdObjectManagement() +{ + +} + +WpdObjectManagement::~WpdObjectManagement() +{ + +} + +HRESULT WpdObjectManagement::Initialize(_In_ FakeDevice *pFakeDevice) +{ + HRESULT hr = S_OK; + + if(pFakeDevice == NULL) + { + hr = E_POINTER; + CHECK_HR(hr, "Cannot have NULL parameter"); + return hr; + } + + m_pFakeDevice = pFakeDevice; + + return hr; +} + +HRESULT WpdObjectManagement::DispatchWpdMessage( + _In_ REFPROPERTYKEY Command, + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + + if (hr == S_OK) + { + if (Command.fmtid != WPD_CATEGORY_OBJECT_MANAGEMENT) + { + hr = E_INVALIDARG; + CHECK_HR(hr, "This object does not support this command category %ws",CComBSTR(Command.fmtid)); + } + } + + if (hr == S_OK) + { + if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_DELETE_OBJECTS.pid) + { + hr = OnDelete(pParams, pResults); + CHECK_HR(hr, "Failed to delete object"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_CREATE_OBJECT_WITH_PROPERTIES_ONLY.pid) + { + hr = OnCreateObjectWithPropertiesOnly(pParams, pResults); + CHECK_HR(hr, "Failed to create object"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_CREATE_OBJECT_WITH_PROPERTIES_AND_DATA.pid) + { + hr = OnCreateObjectWithPropertiesAndData(pParams, pResults); + CHECK_HR(hr, "Failed to create object"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_WRITE_OBJECT_DATA.pid) + { + hr = OnWriteObjectData(pParams, pResults); + CHECK_HR(hr, "Failed to write object data"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_COMMIT_OBJECT.pid) + { + hr = OnCommit(pParams, pResults); + CHECK_HR(hr, "Failed to commit object"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_REVERT_OBJECT.pid) + { + hr = OnRevert(pParams, pResults); + CHECK_HR(hr, "Failed to revert object"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_MOVE_OBJECTS.pid) + { + hr = OnMove(pParams, pResults); + CHECK_HR(hr, "Failed to Move objects"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_COPY_OBJECTS.pid) + { + hr = OnCopy(pParams, pResults); + CHECK_HR(hr, "Failed to Copy objects"); + } + else if (Command.pid == WPD_COMMAND_OBJECT_MANAGEMENT_UPDATE_OBJECT_WITH_PROPERTIES_AND_DATA.pid) + { + hr = OnUpdateObjectWithPropertiesAndData(pParams, pResults); + CHECK_HR(hr, "Failed to update object"); + } + else + { + hr = E_NOTIMPL; + CHECK_HR(hr, "This object does not support this command id %d", Command.pid); + } + } + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_DELETE_OBJECTS + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS: the ObjectIDs, indicating which objects to delete. These may + * contain children. + * - WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_OPTIONS: Flag parameter indicating delete options. Must be one + * of the following: + * - PORTABLE_DEVICE_DELETE_NO_RECURSION - Deletes the + * object only. This should fail if children exist. + * - PORTABLE_DEVICE_DELETE_WITH_RECURSION - Deletes this + * object and all children. + * + * The driver should: + * - If the flag is PORTABLE_DEVICE_DELETE_NO_RECURSION the driver should delete the + * specified object only. If the object still has children the driver should not delete + * the object and instead return HRESULT_FROM_WIN32(ERROR_INVALID_OPERATION). + * - If the flag is PORTABLE_DEVICE_DELETE_WITH_RECURSION the driver should delete the + * specified object and all of its children. + * - Fill out the operation results in WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_RESULTS. It contains an IPortableDevicePropVariantCollection of + * VT_ERROR values indicating the success or failure of the operation for that element. + * Order is implicit, i.e. the first element of WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_RESULTS corresponds to the first element of WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS and so on. + * - The driver should return: + * - S_OK if all objects were deleted successfully. + * - S_FALSE if any object delete failed. + * - An error return indicates that the driver did not delete any objects, and + * WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_RESULTS is ignored. + */ +HRESULT WpdObjectManagement::OnDelete( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + DWORD dwOptions = PORTABLE_DEVICE_DELETE_NO_RECURSION; + BOOL bDeleteFailed = FALSE; + CComPtr<IPortableDevicePropVariantCollection> pObjectIDs; + CComPtr<IPortableDevicePropVariantCollection> pDeleteResults; + CComPtr<IPortableDeviceValues> pEventParams; + VARTYPE vt = VT_EMPTY; + + if (hr == S_OK) + { + hr = pParams->GetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS, &pObjectIDs); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS"); + } + + // Ensure that this is a collection of VT_LPWSTR + if (hr == S_OK) + { + hr = pObjectIDs->GetType(&vt); + CHECK_HR(hr, "Failed to get the VARTYP of WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS"); + if (hr == S_OK) + { + if (vt != VT_LPWSTR) + { + hr = E_INVALIDARG; + CHECK_HR(hr, "WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS is not a collection of VT_LPWSTR"); + } + } + } + + if (hr == S_OK) + { + hr = pParams->GetUnsignedIntegerValue(WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_OPTIONS, &dwOptions); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_OPTIONS"); + } + + if (hr == S_OK) + { + hr = CoCreateInstance(CLSID_PortableDevicePropVariantCollection, + NULL, + CLSCTX_INPROC_SERVER, + IID_IPortableDevicePropVariantCollection, + (VOID**) &pDeleteResults); + CHECK_HR(hr, "Failed to CoCreateInstance CLSID_PortableDevicePropVariantCollection"); + } + + + if (hr == S_OK) + { + DWORD cObjects = 0; + // Loop through the object list and attempt to delete + hr = pObjectIDs->GetCount(&cObjects); + CHECK_HR(hr, "Failed to get number of objects to delete"); + + if (hr == S_OK) + { + for(DWORD dwIndex = 0; dwIndex < cObjects; dwIndex++) + { + HRESULT hrTemp = S_OK; + PROPVARIANT pv = {0}; + + PropVariantInit(&pv); + // Get the next Object to delete + hr = pObjectIDs->GetAt(dwIndex, &pv); + CHECK_HR(hr, "Failed to get next object id at index %d", dwIndex); + if (hr == S_OK) + { + pEventParams = NULL; + // Get the object properties used for sending the event. Ignore errors (these are expected in some cases + // e.g. app is sending object ID that doesn't exist), since this relates to the event, not the delete operation, + // and we return results for the delete (an error posting the event is non-fatal). + hrTemp = m_pFakeDevice->GetObjectPropertiesForEvent(pv.pwszVal, &pEventParams); + CHECK_HR(hrTemp, "Failed to get properties for event on object %ws", pv.pwszVal); + + HRESULT hrDelete = S_OK; + PROPVARIANT pvResult = {0}; + + PropVariantInit(&pvResult); + + hrDelete = m_pFakeDevice->DeleteObject(dwOptions, pv.pwszVal); + CHECK_HR(hrDelete, "Failed to delete object [%ws]", pv.pwszVal); + + if(FAILED(hrDelete)) + { + bDeleteFailed = TRUE; + } + + // Save this result + pvResult.vt = VT_ERROR; + pvResult.scode = hrDelete; + hrTemp = pDeleteResults->Add(&pvResult); + PropVariantClear(&pvResult); + CHECK_HR(hrTemp, "Failed to add result for [%ws] to list of results", pv.pwszVal); + + if ((hrDelete == S_OK) && (hrTemp == S_OK)) + { + HRESULT hrEvent = S_OK; + // Set the event-specific parameters + hrEvent = pEventParams->SetGuidValue(WPD_EVENT_PARAMETER_EVENT_ID, WPD_EVENT_OBJECT_REMOVED); + CHECK_HR(hrEvent, "Failed to add WPD_EVENT_PARAMETER_EVENT_ID"); + + // Send the Event + if (hrEvent == S_OK) + { + PostWpdEvent(pParams, pEventParams); + } + } + PropVariantClear(&pv); + } + else + { + break; + } + } + } + } + + // Set the results + if (hr == S_OK) + { + hr = pResults->SetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_RESULTS, pDeleteResults); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_DELETE_RESULTS"); + } + + // If an object failed to delete, make sure we return S_FALSE + if ((hr == S_OK) && (bDeleteFailed)) + { + hr = S_FALSE; + } + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_MOVE_OBJECTS + * command. + * + * This command will move the specified objects to the destination folder. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS : the ObjectIDs, indicating which objects to move. These may + * be folder objects. + * - WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID: Indicates the destination folder for the move operation. + * + * The driver should: + * - Attempt to Move the object specified in WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS to the folder specified in + * WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID. + * - Fill out the operation results in WPD_PROPERTY_OBJECT_MANAGEMENT_MOVE_RESULTS. It contains an IportableDevicePropVariantCollection of + * VT_ERROR values indicating the success or failure of the operation for that element. + * Order is implicit, i.e. the first element of WPD_PROPERTY_OBJECT_MANAGEMENT_MOVE_RESULTS corresponds to the first element of WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS and so on. + * - The driver should return: + * - S_OK if all objects were moved successfully. + * - S_FALSE if any object move failed. + * - An error return indicates that the driver did not move any objects, and + * WPD_PROPERTY_OBJECT_MANAGEMENT_MOVE_RESULTS is ignored. + */ +HRESULT WpdObjectManagement::OnMove( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszDestFolderObjectID = NULL; + CComPtr<IPortableDevicePropVariantCollection> pObjectIDs; + CComPtr<IPortableDevicePropVariantCollection> pMoveResults; + BOOL bMoveFailed = FALSE; + VARTYPE vt = VT_EMPTY; + + if (hr == S_OK) + { + hr = pParams->GetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS, &pObjectIDs); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS"); + } + + // Ensure that this is a collection of VT_LPWSTR + if (hr == S_OK) + { + hr = pObjectIDs->GetType(&vt); + CHECK_HR(hr, "Failed to get the VARTYP of WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS"); + if (hr == S_OK) + { + if (vt != VT_LPWSTR) + { + hr = E_INVALIDARG; + CHECK_HR(hr, "WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS is not a collection of VT_LPWSTR"); + } + } + } + + if (hr == S_OK) + { + hr = pParams->GetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID, &pszDestFolderObjectID); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID"); + } + + if (hr == S_OK) + { + hr = CoCreateInstance(CLSID_PortableDevicePropVariantCollection, + NULL, + CLSCTX_INPROC_SERVER, + IID_IPortableDevicePropVariantCollection, + (VOID**) &pMoveResults); + CHECK_HR(hr, "Failed to CoCreateInstance CLSID_PortableDevicePropVariantCollection"); + } + + + if (hr == S_OK) + { + DWORD cObjects = 0; + // Loop through the object list and attempt to move + hr = pObjectIDs->GetCount(&cObjects); + CHECK_HR(hr, "Failed to get number of objects to move"); + + if (hr == S_OK) + { + for(DWORD dwIndex = 0; dwIndex < cObjects; dwIndex++) + { + HRESULT hrTemp = S_OK; + PROPVARIANT pv = {0}; + + PropVariantInit(&pv); + // Get the next Object to move + hrTemp = pObjectIDs->GetAt(dwIndex, &pv); + CHECK_HR(hr, "Failed to get next object id at index %d", dwIndex); + if (hrTemp == S_OK) + { + // Move this object + hrTemp = m_pFakeDevice->MoveObject(pv.pwszVal, pszDestFolderObjectID); + CHECK_HR(hrTemp, "Failed to move object [%ws] to folder [%ws]", pv.pwszVal, pszDestFolderObjectID); + + if(FAILED(hrTemp)) + { + bMoveFailed = TRUE; + } + + PROPVARIANT pvResult = {0}; + + PropVariantInit(&pvResult); + + // Save this result + pvResult.vt = VT_ERROR; + pvResult.scode = hrTemp; + hrTemp = pMoveResults->Add(&pvResult); + CHECK_HR(hrTemp, "Failed to add result for [%ws] to list of results", pv.pwszVal); + + PropVariantClear(&pvResult); + } + PropVariantClear(&pv); + } + } + } + + // Set the results + if (hr == S_OK) + { + hr = pResults->SetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_OBJECT_MANAGEMENT_MOVE_RESULTS, pMoveResults); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_MOVE_RESULTS"); + } + + CoTaskMemFree(pszDestFolderObjectID); + + // If an object failed to move, make sure we return S_FALSE + if ((hr == S_OK) && (bMoveFailed)) + { + hr = S_FALSE; + } + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_COPY_OBJECTS + * command. + * + * This command will copy the specified objects to the destination folder. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS : the ObjectIDs, indicating which objects to copy. These may + * be folder objects. + * - WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID: Indicates the destination folder for the copy operation. + * + * The driver should: + * - Attempt to copy the object specified in WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS to the folder specified in + * WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID. + * - Fill out the operation results in WPD_PROPERTY_OBJECT_MANAGEMENT_COPY_RESULTS. It contains an IPortableDevicePropVariantCollection of + * VT_ERROR values indicating the success or failure of the operation for that element. + * Order is implicit, i.e. the first element of WPD_PROPERTY_OBJECT_MANAGEMENT_COPY_RESULTS corresponds to the first element of WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS and so on. + * - The driver should return: + * - S_OK if all objects were copied successfully. + * - S_FALSE if any object copy failed. + * - An error return indicates that the driver did not copy any objects, and + * WPD_PROPERTY_OBJECT_MANAGEMENT_COPY_RESULTS is ignored. + */ +HRESULT WpdObjectManagement::OnCopy( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszDestFolderObjectID = NULL; + CComPtr<IPortableDevicePropVariantCollection> pObjectIDs; + CComPtr<IPortableDevicePropVariantCollection> pCopyResults; + BOOL bCopyFailed = FALSE; + VARTYPE vt = VT_EMPTY; + + hr = pParams->GetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS, &pObjectIDs); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS"); + + // Ensure that this is a collection of VT_LPWSTR + if (SUCCEEDED(hr)) + { + hr = pObjectIDs->GetType(&vt); + CHECK_HR(hr, "Failed to get the VARTYP of WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS"); + if (hr == S_OK) + { + if (vt != VT_LPWSTR) + { + hr = E_INVALIDARG; + CHECK_HR(hr, "WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_IDS is not a collection of VT_LPWSTR"); + } + } + } + + if (SUCCEEDED(hr)) + { + hr = pParams->GetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID, &pszDestFolderObjectID); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_DESTINATION_FOLDER_OBJECT_ID"); + } + + if (SUCCEEDED(hr)) + { + hr = CoCreateInstance(CLSID_PortableDevicePropVariantCollection, + NULL, + CLSCTX_INPROC_SERVER, + IID_IPortableDevicePropVariantCollection, + (VOID**) &pCopyResults); + CHECK_HR(hr, "Failed to CoCreateInstance CLSID_PortableDevicePropVariantCollection"); + } + + + if (SUCCEEDED(hr)) + { + DWORD cObjects = 0; + // Loop through the object list and attempt to copy + hr = pObjectIDs->GetCount(&cObjects); + CHECK_HR(hr, "Failed to get number of objects to copy"); + + if (SUCCEEDED(hr)) + { + for(DWORD dwIndex = 0; dwIndex < cObjects; dwIndex++) + { + CComPtr<IPortableDeviceValues> pEventParams; + LPWSTR pszNewObjectID = NULL; + HRESULT hrTemp = S_OK; + PROPVARIANT pv = {0}; + + PropVariantInit(&pv); + // Get the next Object to copy + hrTemp = pObjectIDs->GetAt(dwIndex, &pv); + CHECK_HR(hr, "Failed to get next object id at index %d", dwIndex); + if (SUCCEEDED(hrTemp)) + { + // Copy this object + hrTemp = m_pFakeDevice->CopyObject(pv.pwszVal, pszDestFolderObjectID, &pszNewObjectID); + CHECK_HR(hrTemp, "Failed to copy object [%ws] to folder [%ws]", pv.pwszVal, pszDestFolderObjectID); + + if(FAILED(hrTemp)) + { + bCopyFailed = TRUE; + } + + PROPVARIANT pvResult = {0}; + + PropVariantInit(&pvResult); + + // Save this result + pvResult.vt = VT_ERROR; + pvResult.scode = hrTemp; + hrTemp = pCopyResults->Add(&pvResult); + CHECK_HR(hrTemp, "Failed to add result for [%ws] to list of results", pv.pwszVal); + + PropVariantClear(&pvResult); + + if (SUCCEEDED(hrTemp)) + { + // Send the event + // Get the object properties used for sending the event + hrTemp = m_pFakeDevice->GetObjectPropertiesForEvent(pszNewObjectID, &pEventParams); + CHECK_HR(hrTemp, "Failed to get properties for event on object %ws", pszNewObjectID); + if (SUCCEEDED(hrTemp)) + { + // Set the event-specific parameters + hrTemp = pEventParams->SetGuidValue(WPD_EVENT_PARAMETER_EVENT_ID, WPD_EVENT_OBJECT_ADDED); + CHECK_HR(hrTemp, "Failed to add WPD_EVENT_PARAMETER_EVENT_ID"); + + // Send the Event + if (SUCCEEDED(hrTemp)) + { + PostWpdEvent(pParams, pEventParams); + } + } + } + } + + if (pszNewObjectID != NULL) + { + CoTaskMemFree(pszNewObjectID); + pszNewObjectID = NULL; + } + + PropVariantClear(&pv); + } + } + } + + // Set the results + if (SUCCEEDED(hr)) + { + hr = pResults->SetIPortableDevicePropVariantCollectionValue(WPD_PROPERTY_OBJECT_MANAGEMENT_COPY_RESULTS, pCopyResults); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_COPY_RESULTS"); + } + + CoTaskMemFree(pszDestFolderObjectID); + + // If an object failed to copy, make sure we return S_FALSE + if (SUCCEEDED(hr) && bCopyFailed) + { + hr = S_FALSE; + } + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_CREATE_OBJECT_WITH_PROPERTIES_ONLY + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES: Contains an IPortableDeviceValues, describing + * properties of the new object. At the very least, it will contain: + * - WPD_OBJECT_NAME: The object name. + * - WPD_PARENT_ID: Identifies the parent object. The object should be inserted as a child of + * this parent (e.g. this would be the target directory in a file system based device). + * + * The driver should: + * - Create the object, and return its ID in WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID. + */ +HRESULT WpdObjectManagement::OnCreateObjectWithPropertiesOnly( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszContext = NULL; + CComPtr<IPortableDeviceValues> pValues; + CComPtr<IPortableDeviceValues> pEventParams; + + // Get the Object Properties + hr = pParams->GetIPortableDeviceValuesValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES, &pValues); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES"); + + // Save the object to the device here. + if (SUCCEEDED(hr)) + { + hr = m_pFakeDevice->SaveNewObject(pValues, &pszContext); + CHECK_HR(hr, "Failed to save new (properties only) object to device"); + } + + if (SUCCEEDED(hr)) + { + hr = pResults->SetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID, pszContext); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID"); + } + + if (SUCCEEDED(hr)) + { + // Send the event + HRESULT hrTemp = S_OK; + // Get the object properties used for sending the event + hrTemp = m_pFakeDevice->GetObjectPropertiesForEvent(pszContext, &pEventParams); + CHECK_HR(hrTemp, "Failed to get properties for event on object %ws", pszContext); + if (SUCCEEDED(hrTemp)) + { + // Set the event-specific parameters + hrTemp = pEventParams->SetGuidValue(WPD_EVENT_PARAMETER_EVENT_ID, WPD_EVENT_OBJECT_ADDED); + CHECK_HR(hrTemp, "Failed to add WPD_EVENT_PARAMETER_EVENT_ID"); + + // Send the Event + if (SUCCEEDED(hrTemp)) + { + PostWpdEvent(pParams, pEventParams); + } + } + } + + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pszContext); + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_CREATE_OBJECT_WITH_PROPERTIES_AND_DATA + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES: Contains an IPortableDeviceValues, describing + * properties of the new object. At the very least, it will contain: + * - WPD_OBJECT_NAME: The object name. + * - WPD_PARENT_ID: Identifies the parent object. The object should be inserted as a child of + * this parent (e.g. this would be the target directory in a file system based device). + * - WPD_OBJECT_SIZE: The total size of the object data stream. + * - WPD_OBJECT_FORMAT: The format the object data stream. + * + * The driver should: + * - Create a context for this operation and return it in WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT. + * - Return the optimal transfer buffer size in WPD_PROPERTY_OBJECT_MANAGEMENT_OPTIMAL_TRANSFER_BUFFER_SIZE. + */ +HRESULT WpdObjectManagement::OnCreateObjectWithPropertiesAndData( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszContext = NULL; + LPWSTR pszObjectName = NULL; + IUnknown* pContextMap = NULL; + CComPtr<IPortableDeviceValues> pValues; + + // Get the Object Properties + hr = pParams->GetIPortableDeviceValuesValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES, &pValues); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES"); + + // Get Object Name from pValues + if (SUCCEEDED(hr)) + { + hr = pValues->GetStringValue(WPD_OBJECT_NAME, &pszObjectName); + CHECK_HR(hr, "Failed to get WPD_OBJECT_NAME"); + } + + // Get the context map + if (SUCCEEDED(hr)) + { + hr = pParams->GetIUnknownValue(PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP, &pContextMap); + CHECK_HR(hr, "Failed to get PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP"); + } + + if (SUCCEEDED(hr)) + { + // Object data will be provided by the caller. So Create a context identifying this + // creation request, since the actual data will come later, and the object can only be saved + // when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_COMMIT_OBJECT call. + hr = CreateObjectManagementContext((ContextMap*) pContextMap, pszObjectName, pValues, false /*bUpdateRequest*/, &pszContext); + CHECK_HR(hr, "Failed to create object management context"); + } + + if (SUCCEEDED(hr)) + { + hr = pResults->SetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT, pszContext); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT"); + } + + // Set the optimal buffer size + if (SUCCEEDED(hr)) + { + hr = pResults->SetUnsignedIntegerValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OPTIMAL_TRANSFER_BUFFER_SIZE, OPTIMAL_BUFFER_SIZE); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_OPTIMAL_TRANSFER_BUFFER_SIZE value"); + } + + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pszContext); + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pszObjectName); + + SAFE_RELEASE(pContextMap); + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_REVERT_OBJECT + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT: identifies the object creation request (driver returned this in + * WPD_COMMAND_OBJECT_MANAGEMENT_CREATE_OBJECT). + * + * The driver should: + * - Free any resources associated with this context/operation. The object should not be saved to the + * device. If a placeholder was created on the device for this operation, it should be removed. + */ +HRESULT WpdObjectManagement::OnRevert( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszContext = NULL; + ContextMap* pContextMap = NULL; + + // There are no return parameters for this operation. + UNREFERENCED_PARAMETER(pResults); + + hr = pParams->GetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT, &pszContext); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID"); + + if (SUCCEEDED(hr)) + { + hr = pParams->GetIUnknownValue(PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP, (IUnknown**)&pContextMap); + CHECK_HR(hr, "Failed to get PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP"); + } + + if (SUCCEEDED(hr)) + { + hr = DestroyObjectManagementContext(pContextMap, pszContext); + CHECK_HR(hr, "Failed to revert object identified by context [%ws]", pszContext); + } + + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pszContext); + + SAFE_RELEASE(pContextMap); + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_COMMIT_OBJECT + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT: identifies the object creation request (driver returned this in + * WPD_COMMAND_OBJECT_MANAGEMENT_CREATE_OBJECT). + * + * The driver should: + * - Commit the object to the device. + * - Return the new/updated ObjectID in WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID. + */ +HRESULT WpdObjectManagement::OnCommit( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszContext = NULL; + ObjectManagementContext* pContext = NULL; + ContextMap* pContextMap = NULL; + CComPtr<IPortableDeviceValues> pEventParams; + + hr = pParams->GetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT, &pszContext); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID"); + + // Get the object management context for this request + if (SUCCEEDED(hr)) + { + hr = GetClientContext(pParams, pszContext, (IUnknown**)&pContext); + CHECK_HR(hr, "Failed to get Object Management Context"); + } + + if (SUCCEEDED(hr)) + { + hr = pParams->GetIUnknownValue(PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP, (IUnknown**)&pContextMap); + CHECK_HR(hr, "Failed to get PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP"); + } + + if (SUCCEEDED(hr)) + { + if (pContext->UpdateRequest) + { + // Update the object + hr = UpdateObject(pContext, pParams, pResults); + } + else + { + // Save the new object + hr = CommitNewObject(pContext, pszContext, pParams, pResults); + } + } + + // We're done with the context + SAFE_RELEASE(pContext); + + // Destroy the context associated with this request, since it is no longer needed + if (SUCCEEDED(hr)) + { + hr = DestroyObjectManagementContext(pContextMap, pszContext); + CHECK_HR(hr, "Failed to destroy object identified by context [%ws]", pszContext); + } + + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pszContext); + + SAFE_RELEASE(pContextMap); + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_WRITE_OBJECT_DATA + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT: identifies the object creation request previsouly started with + * WPD_PROPERTY_OBJECT_MANAGEMENT_CREATE_OBJECT. + * - WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_TO_WRITE: specifies the next number of bytes to write. + * - WPD_PROPERTY_OBJECT_MANAGEMENT_DATA: specifies byte array where the data should be copied from. + * + * The driver should: + * - Write the next WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_TO_WRITE to the resource. + * - Return the number of bytes actually written in WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_WRITTEN. + * It is normally considered an error if this value does not match WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_TO_WRITE. + */ +HRESULT WpdObjectManagement::OnWriteObjectData( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszContext = NULL; + DWORD dwNumBytesToWrite = 0; + DWORD dwNumBytesWritten = 0; + BYTE* pBuffer = NULL; + DWORD cbBuffer = 0; + ObjectManagementContext* pContext = NULL; + + // Get the Context string + hr = pParams->GetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT, &pszContext); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT"); + + // Get the number of bytes to write + if (SUCCEEDED(hr)) + { + hr = pParams->GetUnsignedIntegerValue(WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_TO_WRITE, &dwNumBytesToWrite); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_TO_WRITE"); + } + + // Get the source buffer + if (SUCCEEDED(hr)) + { + hr = pParams->GetBufferValue(WPD_PROPERTY_OBJECT_MANAGEMENT_DATA, &pBuffer, &cbBuffer); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_DATA"); + } + + // Get the resource context for this object creation request + if (SUCCEEDED(hr)) + { + hr = GetClientContext(pParams, pszContext, (IUnknown**)&pContext); + CHECK_HR(hr, "Failed to get Object Management Context"); + } + + // Write the next band of data for this new object request + if (SUCCEEDED(hr)) + { + // TBD: Write bytes to object. This sample driver ignores the data content. + dwNumBytesWritten = dwNumBytesToWrite; + } + + if (SUCCEEDED(hr)) + { + hr = pResults->SetUnsignedIntegerValue(WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_WRITTEN, dwNumBytesWritten); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_NUM_BYTES_WRITTEN"); + } + + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pszContext); + + // Free the memory. CoTaskMemFree ignores NULLs so no need to check. + CoTaskMemFree(pBuffer); + + SAFE_RELEASE(pContext); + + return hr; +} + +/** + * This method is called when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_UPDATE_OBJECT_WITH_PROPERTIES_AND_DATA + * command. + * + * The parameters sent to us are: + * - WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID: Identifies the object to update. + * - WPD_PROPERTY_OBJECT_MANAGEMENT_UPDATE_PROPERTIES: Contains an IPortableDeviceValues, describing + * the updated properties of the object. At the very least, it will contain: + * - WPD_OBJECT_SIZE: The total size of the object data stream. + * + * The driver should: + * - Create a context for this operation and return it in WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT. + * - Return the optimal transfer buffer size in WPD_PROPERTY_OBJECT_MANAGEMENT_OPTIMAL_TRANSFER_BUFFER_SIZE. + */ +HRESULT WpdObjectManagement::OnUpdateObjectWithPropertiesAndData( + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + HRESULT hr = S_OK; + LPWSTR pszContext = NULL; + LPWSTR pszObjectId = NULL; + IUnknown* pContextMap = NULL; + BOOL bUpdatable = NULL; + CComPtr<IPortableDeviceValues> pValues; + + // Get the Object Identifier. + hr = pParams->GetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID, &pszObjectId); + CHECK_HR(hr, "Failed to get WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID"); + + // Get the Object Properties + if (SUCCEEDED(hr)) + { + hr = pParams->GetIPortableDeviceValuesValue(WPD_PROPERTY_OBJECT_MANAGEMENT_UPDATE_PROPERTIES, &pValues); + CHECK_HR(hr, "Missing value for WPD_PROPERTY_OBJECT_MANAGEMENT_CREATION_PROPERTIES"); + } + + // Get the context map + if (SUCCEEDED(hr)) + { + hr = pParams->GetIUnknownValue(PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP, &pContextMap); + CHECK_HR(hr, "Failed to get PRIVATE_SAMPLE_DRIVER_CLIENT_CONTEXT_MAP"); + } + + // Validate whether this object supports the default resource + if (SUCCEEDED(hr)) + { + hr = m_pFakeDevice->SupportsResource(pszObjectId, WPD_RESOURCE_DEFAULT, &bUpdatable); + CHECK_HR(hr, "Failed to check whether object supports WPD_RESOURCE_DEFAULT"); + + if (SUCCEEDED(hr) && !bUpdatable) + { + hr = E_INVALIDARG; + CHECK_HR(hr, "Object does not support WPD_RESOURCE_DEFAULT"); + } + } + + if (SUCCEEDED(hr)) + { + // Object data will be provided by the caller. So Create a context identifying this + // creation request, since the actual data will come later, and the object can only be saved + // when we receive a WPD_COMMAND_OBJECT_MANAGEMENT_COMMIT_OBJECT call. + hr = CreateObjectManagementContext((ContextMap*) pContextMap, pszObjectId, pValues, true /*bUpdateRequest*/, &pszContext); + CHECK_HR(hr, "Failed to create object management context"); + } + + if (SUCCEEDED(hr)) + { + hr = pResults->SetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT, pszContext); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_CONTEXT"); + } + + // Set the optimal buffer size + if (SUCCEEDED(hr)) + { + hr = pResults->SetUnsignedIntegerValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OPTIMAL_TRANSFER_BUFFER_SIZE, OPTIMAL_BUFFER_SIZE); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_OPTIMAL_TRANSFER_BUFFER_SIZE value"); + } + + CoTaskMemFree(pszContext); + CoTaskMemFree(pszObjectId); + + SAFE_RELEASE(pContextMap); + + return hr; +} + +HRESULT WpdObjectManagement::CreateObjectManagementContext( + _In_ ContextMap* pContextMap, + _In_ LPCWSTR pszObjectName, + _In_ IPortableDeviceValues* pObjectProperties, + _In_ BOOL bUpdateRequest, + _Outptr_result_nullonfailure_ LPWSTR* ppszObjectManagementContext) +{ + HRESULT hr = S_OK; + GUID guidContext = GUID_NULL; + ObjectManagementContext* pContext = NULL; + CComBSTR bstrContext; + + if((pContextMap == NULL) || + (pObjectProperties == NULL) || + (pszObjectName == NULL) || + (ppszObjectManagementContext == NULL)) + { + hr = E_POINTER; + CHECK_HR(hr, "Cannot have NULL parameter"); + return hr; + } + + *ppszObjectManagementContext = NULL; + + hr = CoCreateGuid(&guidContext); + if (SUCCEEDED(hr)) + { + bstrContext = guidContext; + if(bstrContext.Length() == 0) + { + hr = E_OUTOFMEMORY; + CHECK_HR(hr, "Failed to create BSTR from GUID"); + } + } + + if (SUCCEEDED(hr)) + { + pContext = new ObjectManagementContext(); + if(pContext != NULL) + { + CAtlStringW strKey = bstrContext; + pContext->Name = pszObjectName; + pContext->ObjectProperties = pObjectProperties; + pContext->UpdateRequest = bUpdateRequest; + + hr = pContextMap->Add(strKey, pContext); + CHECK_HR(hr, "Failed to add object creation context to client context map"); + + pContext->Release(); + } + else + { + hr = E_OUTOFMEMORY; + CHECK_HR(hr, "Failed to allocate object creation context"); + } + } + + if (SUCCEEDED(hr)) + { + *ppszObjectManagementContext = AtlAllocTaskWideString(bstrContext); + } + + return hr; +} + +HRESULT WpdObjectManagement::DestroyObjectManagementContext( + _In_ ContextMap* pContextMap, + _In_ LPCWSTR pszObjectManagementContext) +{ + HRESULT hr = S_OK; + + CAtlStringW strKey = pszObjectManagementContext; + pContextMap->Remove(strKey); + + return hr; +} + +HRESULT WpdObjectManagement::CommitNewObject( + _In_ ObjectManagementContext* pContext, + _In_ LPCWSTR pszContext, + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + LPWSTR pszObjectID = NULL; + + // Save the new object + HRESULT hr = m_pFakeDevice->SaveNewObject(pContext->ObjectProperties, &pszObjectID); + CHECK_HR(hr, "Failed to save new object [%ws] to device", pContext->Name); + + // Let the caller know the new object's ID. + if (SUCCEEDED(hr)) + { + hr = pResults->SetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID, pszObjectID); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID"); + } + + if (SUCCEEDED(hr)) + { + // Send the event + HRESULT hrTemp = S_OK; + CComPtr<IPortableDeviceValues> pEventParams; + + // Get the object properties used for sending the event + hrTemp = m_pFakeDevice->GetObjectPropertiesForEvent(pszObjectID, &pEventParams); + CHECK_HR(hrTemp, "Failed to get properties for event on object %ws", pszObjectID); + + if (SUCCEEDED(hrTemp)) + { + // Set the event-specific parameters + hrTemp = pEventParams->SetGuidValue(WPD_EVENT_PARAMETER_EVENT_ID, WPD_EVENT_OBJECT_ADDED); + CHECK_HR(hrTemp, "Failed to add WPD_EVENT_PARAMETER_EVENT_ID"); + + hrTemp = pEventParams->SetStringValue(WPD_EVENT_PARAMETER_OBJECT_CREATION_COOKIE, pszContext); + CHECK_HR(hrTemp, "Failed to add WPD_EVENT_PARAMETER_OBJECT_CREATION_COOKIE"); + + // Send the Event + if (SUCCEEDED(hr)) + { + PostWpdEvent(pParams, pEventParams); + } + } + } + + CoTaskMemFree(pszObjectID); + + return hr; +} + +HRESULT WpdObjectManagement::UpdateObject( + _In_ ObjectManagementContext* pContext, + _In_ IPortableDeviceValues* pParams, + _In_ IPortableDeviceValues* pResults) +{ + // For convenience, the ObjectID was stored in Name for object updates + LPCWSTR pszObjectId = pContext->Name; + + // Update the object + HRESULT hr = m_pFakeDevice->UpdateContentObject(pszObjectId, pContext->ObjectProperties); + CHECK_HR(hr, "Failed to update object [%ws] to device", pszObjectId); + + // Set the object ID. + if (hr == S_OK) + { + hr = pResults->SetStringValue(WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID, pszObjectId); + CHECK_HR(hr, "Failed to set WPD_PROPERTY_OBJECT_MANAGEMENT_OBJECT_ID"); + } + + if (hr == S_OK) + { + // Send the event + HRESULT hrTemp = S_OK; + CComPtr<IPortableDeviceValues> pEventParams; + + // Get the object properties used for sending the event + hrTemp = m_pFakeDevice->GetObjectPropertiesForEvent(pContext->Name, &pEventParams); + CHECK_HR(hrTemp, "Failed to get properties for event on object %ws", pszObjectId); + + if (hrTemp == S_OK) + { + // Set the event-specific parameters + hrTemp = pEventParams->SetGuidValue(WPD_EVENT_PARAMETER_EVENT_ID, WPD_EVENT_OBJECT_UPDATED); + CHECK_HR(hrTemp, "Failed to add WPD_EVENT_PARAMETER_EVENT_ID"); + + // Send the Event + if (hrTemp == S_OK) + { + PostWpdEvent(pParams, pEventParams); + } + } + } + + return hr; +} |
