C语言利用Windows Portable Devices API访问安卓设备文件
C语言利用Windows Portable Devices API访问安卓设备文件
安卓手机刚问世不久时,将手机连接到电脑是通过UMS功能,在电脑端看起来手机就像一个U盘(读取速度也像!),里面的文件一览无余。近五年来,出于对手机文件的安全性可靠性的维护,安卓系统不再提供Windows端对手机存储的全权访问接口,而是提供了MTP协议或PTP协议进行文件传输。
MTP协议是目前最有效的访问安卓设备文件的协议,可以实现对文件的拷贝、删除功能。缺陷在于访问速度慢,延迟较高,且无法使用常规的C/C++文件操作函数实现其访问,无法从命令行访问其内容(关于MTP带来的问题,请看文末链接)。
本文将介绍利用Windows Portable Devices API(WPD API)访问通过MTP协议连接到电脑的安卓设备文件,实现在Windows端的根据名称查找设备、设备中特定文件夹文件遍历和设备文件拷贝到Windows主机。
编程注意事项
-
MTP协议连接到主机的设备上,所有文件都用一个ID来标识。访问文件也使用的是ID,而不是文件的路径,这和访问硬盘上的文件是很不一样的。因此,打开MTP设备时需要根据设备名获取设备的ID,访问文件时需要根据文件名获取文件的ID,在WPD API的操作中,文件路径字符串从来只是次要的,文件的ID字符串才是主要的。我们将在后面的例程中看到这一点。
-
使用WPD API时,里面的所有函数都使用宽字符串(
std::wstring
或WCHAR
)。开发者在调用时需要正确声明字符串类型。
例如,WPD的结构体IPortableDeviceProperties
的成员函数GetValues
的定义:
virtual HRESULT STDMETHODCALLTYPE GetValues( /* [in] */ __RPC__in LPCWSTR pszObjectID,/* [unique][in] */ __RPC__in_opt IPortableDeviceKeyCollection *pKeys,/* [out] */ __RPC__deref_out_opt IPortableDeviceValues **ppValues) = 0;
参数pszObjectID
的类型便是LPCWSTR
,也就是wchar_t*
宽字符串指针。
例程
头文件包含
要使用WPD API,需要包含portabledeviceapi.h
和portabledevice.h
两个头文件,并链接到PortableDeviceGuids.lib
库。
#include <portabledeviceapi.h>
#include <portabledevice.h>
#pragma comment(lib, "PortableDeviceGuids.lib")
根据设备名查找设备
安卓设备连接到电脑后,在设备上选取“MTP传输文件”或“传输文件”,随后在“我的电脑”界面会出现设备名称,例如我的:
该名称即用于编程时的设备查找。
下面给出根据设备名查找设备,并获取设备的编号的代码:
bool FindDevice(const std::wstring& targetDeviceName, std::wstring& targetDeviceId)
{ComPtr<IPortableDeviceManager> pDeviceManager;HRESULT hr = CoCreateInstance(CLSID_PortableDeviceManager,NULL,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&pDeviceManager));if (!SUCCEEDED(hr)) { std::cerr << "Create Device Manager Failed.\n"; return false; }DWORD dwCount = 0;pDeviceManager->GetDevices(NULL, &dwCount);if (dwCount == 0) { std::cerr << "No Device Found.\n"; return false; }PWSTR* pDeviceIDs = new PWSTR[dwCount];for (DWORD i = 0; i < dwCount; i++) {pDeviceIDs[i] = nullptr; // Initialize each pointer to nullptr}hr = pDeviceManager->GetDevices(pDeviceIDs, &dwCount);if (!SUCCEEDED(hr)) {std::cerr << "Getting Device Failed.\n";for (DWORD i = 0; i < dwCount; i++)CoTaskMemFree(pDeviceIDs[i]);delete[] pDeviceIDs;return false;}// For each device found, display the devices friendly name,// manufacturer, and description strings.int dwIndex = 0;for (dwIndex = 0; dwIndex < dwCount; dwIndex++){if (pDeviceIDs[dwIndex] == nullptr)continue;printf("[%d] ", dwIndex);DisplayFriendlyName(pDeviceManager.Get(), pDeviceIDs[dwIndex]);printf(" ");DisplayManufacturer(pDeviceManager.Get(), pDeviceIDs[dwIndex]);printf(" ");DisplayDescription(pDeviceManager.Get(), pDeviceIDs[dwIndex]);PWSTR strName = nullptr;GetDeviceFriendlyName(pDeviceManager.Get(), pDeviceIDs[dwIndex], &strName);if (std::wstring(strName) == targetDeviceName){targetDeviceId = pDeviceIDs[dwIndex];delete[] strName;break;}delete[] strName;}if (dwIndex == dwCount){std::wcout << "Target device " << targetDeviceName.c_str() << " not found!\n";for (DWORD i = 0; i < dwCount; i++)CoTaskMemFree(pDeviceIDs[i]);delete[] pDeviceIDs;return false;}targetDeviceId = pDeviceIDs[dwIndex];for (DWORD i = 0; i < dwCount; i++)CoTaskMemFree(pDeviceIDs[i]);delete[] pDeviceIDs;return true;
}
调用方法:
头文件包含略去
std::wstring device_name=L"REDMI Turbo 4 Pro";
std::wstring device_id;
FindDevice(device_name, device_id);
std::wcout<<device_id<<std::endl;后略
遍历某文件夹下的所有文件
获取了设备ID后,便可以访问设备中的文件夹。假如要访问相机文件夹"内部存储设备/DCIM/Camera"
中的文件,那么应该:
- 打开设备
- 获取文件夹的ID
- 到文件夹遍历所有文件。
例程如下:
打开设备:
上略IPortableDevice* pDevice;HRESULT hr = CoCreateInstance(CLSID_PortableDeviceFTM,NULL,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&pDevice));if (!SUCCEEDED(hr)) { std::cerr << "Create Device Failed.\n"; return false; }IPortableDeviceValues* pClientInfo;CreateClientInfo(&pClientInfo);hr = pDevice->Open(deviceId.c_str(), pClientInfo);if (!SUCCEEDED(hr)) { pDevice = nullptr; std::cerr << "Getting Device Failed.\n"; return false; }
下略
成功打开设备后,我们继续使用指针pDevice
。
获取文件夹ID的函数实现如下:
bool MTPDeviceMonitor::ScanDeviceContents(IPortableDevice* pDevice, const std::wstring& targetFolder, std::wstring& dir_id)
{if (pDevice == nullptr) { std::cerr << "ScanDeviceContents: pDevice is nullptr!\n"; return false; }ComPtr<IPortableDeviceContent> pContent;pDevice->Content(&pContent);ComPtr<IPortableDeviceProperties> pProperties;pContent->Properties(&pProperties);ComPtr<IEnumPortableDeviceObjectIDs> pEnumObjects;pContent->EnumObjects(0, WPD_DEVICE_OBJECT_ID, nullptr, &pEnumObjects);ULONG fetched = 0;PWSTR childId = NULL;std::wstring wstrId=L"";DWORD correct_level = 0;/// Levels of the found directories.auto path_names = SplitStringBySlash(targetFolder);for (auto name : path_names){/// Go to the target directorywhile (SUCCEEDED(pEnumObjects->Next(1, &childId, &fetched)) && fetched == 1){ComPtr<IPortableDeviceValues> pValues;pProperties->GetValues(childId, NULL, &pValues);// 获取文件名和类型PWSTR fileName = NULL;pValues->GetStringValue(WPD_OBJECT_NAME, &fileName);if (std::wstring(fileName) == name){++correct_level;wstrId = childId;pContent->EnumObjects(0, childId, nullptr, &pEnumObjects);CoTaskMemFree(fileName);CoTaskMemFree(childId);break;}CoTaskMemFree(fileName);CoTaskMemFree(childId);}}dirId = wstrId;if (wstrId == L"" || correct_level < path_names.size()) { std::cerr << "Failed to fetch target directory's object id.\n"; return false; }return true;
}
其中,参数dir_id
是获取到的文件夹ID。
遍历文件夹中的文件的函数定义如下:
std::vector<std::wstring> get_file_ids(IPortableDevice* pDevice, std::wstring& dirId)
{if (pDevice == nullptr) { std::cerr << "pDevice is nullptr!\n"; return std::vector<MTPFileInfo>(); }std::vector<std::wstring> files;ComPtr<IEnumPortableDeviceObjectIDs> pEnumObjects;ComPtr<IPortableDeviceProperties> pProperties;ComPtr<IPortableDeviceContent> pContent;pDevice->Content(&pContent);pContent->Properties(&pProperties);pContent->EnumObjects(0, dirId.c_str(), nullptr, &pEnumObjects);ULONG fetched = 0;PWSTR childId = NULL;while (SUCCEEDED(pEnumObjects->Next(1, &childId, &fetched)) && fetched == 1){ComPtr<IPortableDeviceValues> pValues;HRESULT hr = pProperties->GetValues(childId, NULL, &pValues);if (!SUCCEEDED(hr))continue;// 获取文件名和类型PWSTR fileName = NULL;pValues->GetStringValue(WPD_OBJECT_NAME, &fileName); 文件名,但在本示例中不记录文件名GUID contentType;pValues->GetGuidValue(WPD_OBJECT_CONTENT_TYPE, &contentType);// 检查是否为文件夹if (contentType != WPD_CONTENT_TYPE_FOLDER){// Fetch file size from propVar files.push_back(childId);/// 记录文件的ID}CoTaskMemFree(fileName);CoTaskMemFree(childId);}return files;
}
如此一来,便获得了文件夹下的文件ID列表。要获得特定文件的ID,仅需要对比fileName即可。获取了文件ID后便可进行文件操作,请看下节。
从安卓MTP设备拷贝文件到计算机
为实现文件拷贝,除了包含WPD头文件之外,还需以下两句代码包含和链接Shlwapi
:
#include <Shlwapi.h>
#pragma comment(lib, "Shlwapi.lib")
拷贝文件依然需要先得到文件的ID,再通过Shlwapi实现文件数据的传输。示例代码如下:
// 从设备拷贝文件到本地HRESULT WPD_CopyFileFromDevice(IPortableDevice* pDevice, // 已连接的设备指针PCWSTR pObjectID, // 设备文件的IDPCWSTR pLocalSavePath // 本地保存路径(完整路径,如 L"C:\\Downloads\\File.txt")) {IPortableDeviceContent* pContent;HRESULT hr = pDevice->Content(pContent);if (!pContent)return E_INVALIDARG;HRESULT hr = S_OK;IStream* pDeviceStream = nullptr;IStream* pLocalStream = nullptr;// 1. 打开设备文件流IPortableDeviceResources* pResources = nullptr;hr = pContent->Transfer(&pResources);if (SUCCEEDED(hr)) {DWORD optimalBufferSize = 0;hr = pResources->GetStream(pObjectID, WPD_RESOURCE_DEFAULT, STGM_READ,&optimalBufferSize, &pDeviceStream);pResources->Release();}// 2. 创建本地文件流if (SUCCEEDED(hr)) {hr = SHCreateStreamOnFileEx(pLocalSavePath, STGM_CREATE | STGM_WRITE,FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &pLocalStream);}// 3. 传输数据if (SUCCEEDED(hr)) {BYTE buffer[4096];ULONG bytesRead = 0;while (SUCCEEDED(pDeviceStream->Read(buffer, sizeof(buffer), &bytesRead)) && bytesRead > 0){ULONG bytesWritten = 0;hr = pLocalStream->Write(buffer, bytesRead, &bytesWritten);if (FAILED(hr) || bytesWritten != bytesRead) {hr = E_FAIL; // 写入失败break;}}}// 4. 清理资源if (pDeviceStream) pDeviceStream->Release();if (pLocalStream) pLocalStream->Release();return hr;}
利用该函数,可以实现文件从安卓设备到Windows主机的拷贝。
参考文档
WPD应用编程接口
IPortableDeviceDataStream
MTP模式与USB存储模式
How do I access MTP devices on the command line in Windows?
how to access my Android phone from my terminal?
dokany,另一种操作MTP设备的方案
使用dokany的mirror.exe挂载盘符