当前位置: 首页 > java >正文

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主机。

编程注意事项

  1. MTP协议连接到主机的设备上,所有文件都用一个ID来标识。访问文件也使用的是ID,而不是文件的路径,这和访问硬盘上的文件是很不一样的。因此,打开MTP设备时需要根据设备名获取设备的ID,访问文件时需要根据文件名获取文件的ID,在WPD API的操作中,文件路径字符串从来只是次要的,文件的ID字符串才是主要的。我们将在后面的例程中看到这一点。

  2. 使用WPD API时,里面的所有函数都使用宽字符串(std::wstringWCHAR)。开发者在调用时需要正确声明字符串类型。

例如,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.hportabledevice.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"中的文件,那么应该:

  1. 打开设备
  2. 获取文件夹的ID
  3. 到文件夹遍历所有文件。

例程如下:

打开设备:

 上略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挂载盘符

http://www.xdnf.cn/news/8980.html

相关文章:

  • 什么是HTTP HTTP 和 HTTPS 的区别
  • 视频画质等级
  • openpi π₀ 项目部署运行逻辑(三)——策略推理服务器 serve_policy.py
  • 中小企业AI算力如何选?【显卡租赁】VS【自建服务器】
  • 语音识别——文本转语音
  • 5.26 day 29
  • 论文阅读:Self-Planning Code Generation with Large Language Models
  • AOSP编译错误
  • Linux云计算训练营笔记day16(Linux周期性计划任务、Python)
  • OpenCV (C/C++) 中使用 Sobel 算子进行边缘检测
  • 【读书笔记】《编码:隐匿在计算机软硬件背后的语言》02 门
  • 【杂谈】------使用 __int128 处理超大整数计算
  • Haproxy 基础知识点
  • Halo:一个强大易用的国产开源建站工具
  • kafka实践与C++操作kafka
  • (自用)Java学习-5.14(注册,盐值加密,模糊查询)
  • Vue-模版绑定指令语法/什么是Vue组件
  • 小巧高效的目录索引生成软件
  • 「AR眼镜+智慧应急管理平台+视频联网」——矿山能源数智化转型的“安全之眼”与“效率引擎”
  • ffmpeg转换竖屏(画面是横屏旋转90度的竖屏文件格式)视频到横屏
  • SBT开源构建工具
  • 萤石云实际视频实时接入(生产环境)
  • Milvus分区-分片-段结构详解与最佳实践
  • java写一个简单的冒泡排序
  • 鸿蒙OSUniApp 制作简单的页面跳转与参数传递功能#三方框架 #Uniapp
  • 前端性能优化:如何让网页加载更快?
  • Oracle SHARED POOLRESERVED FREE LIST
  • OWA登录问题分析与解决方案
  • Vite 介绍
  • 【算法提升】牛牛冲钻五 最长无重复子数组 重排字符串 one_day