问题引入
最近处理了一些Android 11当中的系统问题:
- MTP模式下重启设备,在PC端会显示两个手机的内存
- 已插SD卡,MTP模式查看PC盘符不显示SD卡
- MTP模式下,单个文件大小1G及以上大小无法从电脑传输到设备
- 插入SD卡后下滑状态栏点击设置SD卡,会导致SD卡不识别
以上问题均与Android中的MTP功能有关,因此需要先对MTP有一个简单的认识。
认识MTP
MTP(Media Transfer Protocol),媒体传输协议,是微软为计算机和便携式设配之间传输图像、音乐所制定的一种协议,基于PTP(Picture Transfer Protocol).
MTP应用分为两种角色,一个是Initiator,另一个是Responder。
Initiator:在MTP中所有的请求都是由Initiator发起。例如,PC请求获取Android平板电脑上的文件数据。
Responder:它会处理Initiator的请求。除此之外,Responder也会发送Event事件。
Android中MTP框架
框架中的重要角色
kernel层
USB驱动:负责数据交换,即Android设配和PC通过USB数据线连接之后,数据经过USB数据线发送给USB驱动;
MTP驱动:负责和上层通信,以及和USB驱动通信。
对于上层而言,MTP驱动会从USB驱动中解析出的请求数据,然后传递给上层,上层收到数据后会给底层发送反馈。而对于底层来说,MTP驱动也会将来自上层的反馈内容打包好之后,传递给USB驱动。
Framework-JNI层
MtpServer
:会不断的监听Kernel的MTP请求消息,并对相应的消息进行相关的处理。同时,MTP的Event事件也是通过MtpServer
发送给MTP驱动的。
MtpStorage
:对应一个存储单元。例如,SD卡就对应一个MtpStorage
。
MtpPacket
和MtpEventPacket
:负责对MTP消息进行打包。
android_mtp_MtpServer
:是JNI层的MtpServer
和Java层的MtpServer
沟通的桥梁。android_mtp_MtpDatabase
实现对java层MtpDatabase
的操作。
framework-Java层
MtpServer
:相当于一个服务器,它通过和底层进行通信从而提供了MTP的相关服务。
MtpDatabase
:充当着数据库的功能,但它本身并没有数据库对数据进行保存,本质上是通过MediaProvider
数据库获取所需要的数据。
MtpStorage
:和JNI层的MtpStorage
相对应。
Application层
MtpReceiver
:负责接收广播,接收到广播后会启动/关闭MtpService
。例如,MtpReceiver
收到Android设备和PC连上的消息时,会启动MtpService
。
MtpService
:提供管理MTP的服务,它会启动MtpServer
,以及将本地存储内容和MTP的内容同步。
MediaProvider
:在MTP中的角色,是本地存储内容查找和本地内容同步。例如,本地新增一个文件时,MediaProvider
会通知MtpServer
,从而进行MTP数据同步。
MTP启动流程
当设备通过USB数据线连接PC时,USB驱动会向上层发送USB状态变化的消息,上层交由UsbDeviceManager
处理。
java层处理流程
JNI层处理流程
在MtpDevHandle
中有一个while死循环,会不断的从Mtp驱动节点/dev/mtp_usb
中读取数据,并做相应的处理,这便是Mtp持续工作的核心动力。
实战分析过程
问题1:MTP模式下重启设备,在PC端会显示两个手机的内存
这是典型的数据重复问题,上面说到,一个存储单元相当于是一个MtpStorage
,因此,极有可能是重复添加了MtpStorage
,在addStorage()
的代码处打上log即可验证
java
public void addStorage(StorageVolume storage) {
//查看此处log,看是否重复添加
Log.d("jasonwan","addStorage,storage.getPath():"+storage.getPath());
MtpStorage mtpStorage = mManager.addMtpStorage(storage);
mStorageMap.put(storage.getPath(), mtpStorage);
if (mServer != null) {
mServer.addStorage(mtpStorage);
}
}
最终得到的log如下
说明确实重复添加了
解决办法:添加前,判断mStorageMap
中是否已经存在此MtpStorage
java
public void addStorage(StorageVolume storage) {
//数据去重
if (mStorageMap.containsKey(storage.getPath())){
return;
}
MtpStorage mtpStorage = mManager.addMtpStorage(storage);
mStorageMap.put(storage.getPath(), mtpStorage);
if (mServer != null) {
mServer.addStorage(mtpStorage);
}
}
这实际上是Google原生代码的一个漏洞,正常代码设计都会考虑数据重复的问题。
问题2:已插SD卡,MTP模式查看PC盘符不显示SD卡
根据问题1的经验,不显示SD卡,那就是没有将SD卡进行addStorage()
,打log可验证
java
private synchronized void startServer(StorageVolume primary, String[] subdirs) {
//省略部分代码
synchronized (MtpService.class) {
//省略部分代码
final MtpServer server =
new MtpServer(database, fd, mPtpMode,
new OnServerTerminated(), Build.MANUFACTURER,
Build.MODEL, "1.0");
database.setServer(server);
sServerHolder = new ServerHolder(server, database);
// Add currently mounted and enabled storages to the server
if (mUnlocked) {
if (mPtpMode) {
addStorage(primary);
} else {
for (StorageVolume v : mVolumeMap.values()) {
//打印每一个StorageVolume
Log.d("jasonwan","MtpService->startServer: v="+v.toString());
addStorage(v);
}
}
}
server.start();
}
}
得到的log如下:
正常log为:
确实没有打印sd卡的数据,这是需要从数据源查找,数据源位于StorageManagerService.java->getVolumeList()
,这里涉及到跨进程通信,MtpService
与StorageManagerService
通信
java
@Override
public StorageVolume[] getVolumeList(int uid, String packageName, int flags) {
//省略部分代码
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
final String volId = mVolumes.keyAt(i);
final VolumeInfo vol = mVolumes.valueAt(i);
//打印每一个volume数据,该数据由MTP驱动发给上层
Log.d("jasonwan", "vol="+vol.toString());
switch (vol.getType()) {
case VolumeInfo.TYPE_PUBLIC:
case VolumeInfo.TYPE_STUB:
break;
case VolumeInfo.TYPE_EMULATED:
if (vol.getMountUserId() == userId) {
break;
}
// Skip if emulated volume not for userId
default:
continue;
}
boolean match = false;
//省略部分代码
}
//省略部分代码
}
//省略部分代码
return storageVolumes;
}
将MTP驱动发给上层的每一个Volume数据打印出来,看看到底有没有SD卡,log如下:
发现log里面打印了SD卡的信息,说明MTP驱动确实将SD卡的数据发给上层了,但里面有一个标识位mountFlags
为0,即挂在状态为不可见,如果手动将它设置2(可见状态值),看看会不会显示
java
@Override
public StorageVolume[] getVolumeList(int uid, String packageName, int flags) {
//省略部分代码
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
final String volId = mVolumes.keyAt(i);
final VolumeInfo vol = mVolumes.valueAt(i);
//打印每一个volume数据,该数据由MTP驱动发给上层
Log.d("jasonwan", "vol="+vol.toString());
switch (vol.getType()) {
case VolumeInfo.TYPE_PUBLIC:
case VolumeInfo.TYPE_STUB:
break;
case VolumeInfo.TYPE_EMULATED:
if (vol.getMountUserId() == userId) {
break;
}
// Skip if emulated volume not for userId
default:
continue;
}
boolean match = false;
//手动强行将挂载状态设为可见
if (vol.type==VolumeInfo.TYPE_PUBLIC){
vol.mountFlags=VolumeInfo.MOUNT_FLAG_VISIBLE;
}
//省略部分代码
}
//省略部分代码
}
//省略部分代码
return storageVolumes;
}
编译写入,得到log如下:
状态值确实为VISIBLE
了,并且PC上也可以显示SD卡
现在需要看看mountFlags的赋值,赋值处位于StorageManagerService.java->onVolumeCreatedLocked()
,即Volume创建时
java
@GuardedBy("mLock")
private void onVolumeCreatedLocked(VolumeInfo vol) {
//省略部分代码
if (vol.type == VolumeInfo.TYPE_EMULATED) {
final StorageManager storage = mContext.getSystemService(StorageManager.class);
final VolumeInfo privateVol = storage.findPrivateForEmulated(vol);
if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)
&& VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) {
Slog.v(TAG, "Found primary storage at " + vol);
//赋值处1
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
} else if (Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) {
Slog.v(TAG, "Found primary storage at " + vol);
//赋值处2
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
}
} else if (vol.type == VolumeInfo.TYPE_PUBLIC) {
// TODO: only look at first public partition
boolean b = Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
&& vol.disk.isDefaultPrimary();
Log.d("jasonwan", "vol.id="+vol.getId()+", mPrimaryStorageUuid="+mPrimaryStorageUuid+", vol.disk.flag="+vol.disk.flags+", result1="+b);
//赋值处3
if (b) {
Slog.v(TAG, "Found primary storage at " + vol);
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
}
// Adoptable public disks are visible to apps, since they meet
// public API requirement of being in a stable location.
Log.d("jasonwan", "result2="+vol.disk.isAdoptable());
if (vol.disk.isAdoptable()) {
//赋值处4
vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
}
vol.mountUserId = mCurrentUserId;
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
} else if (vol.type == VolumeInfo.TYPE_PRIVATE) {
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
} else if (vol.type == VolumeInfo.TYPE_STUB) {
vol.mountUserId = mCurrentUserId;
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
} else {
Slog.d(TAG, "Skipping automatic mounting of " + vol);
}
}
赋值处总共有四处,排除1、2处(因为SD卡的type属于VolumeInfo.TYPE_PUBLIC
),那么mountFlags
的值取决于vol.disk.isDefaultPrimary()
和vol.disk.isAdoptable()
,而这两个方法都与DiskInfo
的flags
字段有关
java
@UnsupportedAppUsage
public boolean isDefaultPrimary() {
return (flags & FLAG_DEFAULT_PRIMARY) != 0;
}
@UnsupportedAppUsage
public boolean isAdoptable() {
return (flags & FLAG_ADOPTABLE) != 0;
}
从以上代码来看,mountFlags
要想被赋值为VolumeInfo.MOUNT_FLAG_VISIBLE
,flags
的值必须小于4
DiskInfo
来源于Disk创建,打印一下flags
的值
java
@Override
public void onDiskCreated(String diskId, int flags) {
synchronized (mLock) {
Log.d("jasonwan", "onDiskCreated: diskId="+diskId+", flags="+flags);
final String value = SystemProperties.get(StorageManager.PROP_ADOPTABLE);
switch (value) {
case "force_on":
flags |= DiskInfo.FLAG_ADOPTABLE;
break;
case "force_off":
flags &= ~DiskInfo.FLAG_ADOPTABLE;
break;
}
mDisks.put(diskId, new DiskInfo(diskId, flags));
}
}
log如下:
其值小于为4,那么mountFlags不会被赋值,默认值即可0,即不可见状态。深入到native层,DiskInfo.flags
的赋值位于system\vold\VolumeManager.cpp->handleBlockEvent()
cpp
void VolumeManager::handleBlockEvent(NetlinkEvent* evt) {
std::lock_guard<std::mutex> lock(mLock); if (mDebug) {
LOG(DEBUG) << "----------------";
LOG(DEBUG) << "handleBlockEvent with action " << (int)evt->getAction();
evt->dump();
}
std::string eventPath(evt->findParam("DEVPATH") ? evt->findParam("DEVPATH") : "");
std::string devType(evt->findParam("DEVTYPE") ? evt->findParam("DEVTYPE") : "");
//设备类型不是磁盘就返回
if (devType != "disk") return;
//major和minor分别表示该设备的主次设备号,二者联合起来可以识别一个设备
int major = std::stoi(evt->findParam("MAJOR"));
int minor = std::stoi(evt->findParam("MINOR"));
dev_t device = makedev(major, minor);
switch (evt->getAction()) {
case NetlinkEvent::Action::kAdd: {
for (const auto& source : mDiskSources) {
if (source->matches(eventPath)) {
// For now, assume that MMC and virtio-blk (the latter is
// specific to virtual platforms; see Utils.cpp for details)
// devices are SD, and that everything else is USB
int flags = source->getFlags();
//flags赋值
if (major == kMajorBlockMmc || IsVirtioBlkDevice(major)) {
flags |= android::vold::Disk::Flags::kSd;
} else {
flags |= android::vold::Disk::Flags::kUsb;
}
//新建一个Disk,用来保存当前磁盘信息
auto disk =
new android::vold::Disk(eventPath, device, source->getNickname(), flags);
handleDiskAdded(std::shared_ptr<android::vold::Disk>(disk));
break;
}
}
break;
}
case NetlinkEvent::Action::kChange: {
LOG(DEBUG) << "Disk at " << major << ":" << minor << " changed";
handleDiskChanged(device);
break;
}
case NetlinkEvent::Action::kRemove: {
handleDiskRemoved(device);
break;
}
default: {
LOG(WARNING) << "Unexpected block event action " << (int)evt->getAction();
break;
}
}
}
这里是MTP驱动相关的代码,作为一个framework开发者,着实有点看不太懂,后交由驱动同事协助排查。最终排查结果是因为驱动配置不对,调整配置后就好了(驱动代码为公司内部代码,这里就不贴了)。
问题3:MTP模式下,单个文件大小1G及以上大小无法从电脑传输到设备
传输失败的关键日志如下:
传输断开时发现MTPserver服务报错,找到报错代码处
上面刚刚提到,while死循环是Mtp持续工作的核心动力,看看handleRequest()
中如何处理MTP驱动发来的请求
该函数根据用户不同的操作,如复制文件、打开文件,查看存储卷信息等,做对应的处理,复制文件属于MTP_OPERATION_SEND_OBJECT
,交由doSendObject()
处理
log中出现的错误日志正是来自此方法,从结果得知,receiveFile()
返回的值小于0,看此函数的处理,位于\frameworks\av\media\mtp\MtpFfsHandle.cpp->receiveFile()
中,通过埋日志,得知错误日志来源于以下代码逻辑
这里的代码逻辑根据注释得知,此处必须等待所有事件处理完毕,在此过程中,数据传输随时可能结束,从日志得知,waitEvents()
函数返回了-1,查看此函数的逻辑
根据日志,在handleEvent()
函数处理某个事件时返回了-1,继续看是什么事件
根据日志,在处理FUNCTION_SETUP
事件时返回了-1,且处理该事件的handleControlRequest()
函数也返回了-1,继续深入查看
可见,最终原因是因为收到了一条MTP_REQ_ECANCELED
事件,然后系统主动取消了此次操作,猜测可能是传输文件太大触发了底层某种机制,找驱动同事协助排查,最后排查结果是因为内存越界导致的。解决办法就是驱动层将直通改为地址映射。
问题4:插入SD卡后下滑状态栏点击设置SD卡,会导致SD卡不识别
正常点击应该进入SD卡设置页面,但这里点击会直接弹出SD卡,直接找到对应的代码frameworks/base/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
上图为通知栏的显示逻辑,点击通知栏会通过unmountIntent
执行com.android.car.settings.storage.StorageUnmountReceiver
里面的逻辑
而StorageUnmountReceiver
里面执行了卸载sd的异步任务
可见Automotive修改了这里的逻辑,点击就是卸载SD卡,所以无需修改。
总结
通过处理这些问题,个人对Android中的MTP设计,USB设计都有了深入的了解,并且加深理解了Android架构中上层和底层之间的通信。