Android Framework开发实战--USB/Mtp/SDCard问题处理

1,797 阅读9分钟

问题引入

最近处理了一些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事件。

MTP功能界面

Android中MTP框架

mtp框架

框架中的重要角色

kernel层

USB驱动:负责数据交换,即Android设配和PC通过USB数据线连接之后,数据经过USB数据线发送给USB驱动;

MTP驱动:负责和上层通信,以及和USB驱动通信。

对于上层而言,MTP驱动会从USB驱动中解析出的请求数据,然后传递给上层,上层收到数据后会给底层发送反馈。而对于底层来说,MTP驱动也会将来自上层的反馈内容打包好之后,传递给USB驱动。

mtp示意图

Framework-JNI层

MtpServer:会不断的监听Kernel的MTP请求消息,并对相应的消息进行相关的处理。同时,MTP的Event事件也是通过MtpServer发送给MTP驱动的。

MtpStorage:对应一个存储单元。例如,SD卡就对应一个MtpStorage

MtpPacketMtpEventPacket:负责对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层处理流程

mtp启动流程-java

JNI层处理流程

mtp启动流程-native

MtpDevHandle中有一个while死循环,会不断的从Mtp驱动节点/dev/mtp_usb中读取数据,并做相应的处理,这便是Mtp持续工作的核心动力。

实战分析过程

问题1:MTP模式下重启设备,在PC端会显示两个手机的内存

image-20231101145427704

这是典型的数据重复问题,上面说到,一个存储单元相当于是一个MtpStorage,因此,极有可能是重复添加了MtpStorage,在addStorage()的代码处打上log即可验证

    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如下

image-20231101145700985

说明确实重复添加了

解决办法:添加前,判断mStorageMap中是否已经存在此MtpStorage

    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可验证

    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如下:

mtp无法显示sd卡log

正常log为:

mtp无法显示sd卡正常log

确实没有打印sd卡的数据,这是需要从数据源查找,数据源位于StorageManagerService.java->getVolumeList(),这里涉及到跨进程通信,MtpServiceStorageManagerService通信

 	@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(可见状态值),看看会不会显示

 	@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卡

挂载后pc端可见

现在需要看看mountFlags的赋值,赋值处位于StorageManagerService.java->onVolumeCreatedLocked(),即Volume创建时

    @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(),而这两个方法都与DiskInfoflags字段有关

    @UnsupportedAppUsage
    public boolean isDefaultPrimary() {
        return (flags & FLAG_DEFAULT_PRIMARY) != 0;
    }

    @UnsupportedAppUsage
    public boolean isAdoptable() {
        return (flags & FLAG_ADOPTABLE) != 0;
    }

从以上代码来看,mountFlags要想被赋值为VolumeInfo.MOUNT_FLAG_VISIBLEflags的值必须小于4

DiskInfo来源于Disk创建,打印一下flags的值

        @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如下:

打印diskinfo.flags的值

其值小于为4,那么mountFlags不会被赋值,默认值即可0,即不可见状态。深入到native层,DiskInfo.flags的赋值位于system\vold\VolumeManager.cpp->handleBlockEvent()

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及以上大小无法从电脑传输到设备

传输失败的关键日志如下:

mtp传输失败log

传输断开时发现MTPserver服务报错,找到报错代码处

传输失败关键代码1

上面刚刚提到,while死循环是Mtp持续工作的核心动力,看看handleRequest()中如何处理MTP驱动发来的请求

传输失败关键代码2

该函数根据用户不同的操作,如复制文件、打开文件,查看存储卷信息等,做对应的处理,复制文件属于MTP_OPERATION_SEND_OBJECT,交由doSendObject()处理

传输失败关键代码3

log中出现的错误日志正是来自此方法,从结果得知,receiveFile()返回的值小于0,看此函数的处理,位于\frameworks\av\media\mtp\MtpFfsHandle.cpp->receiveFile()中,通过埋日志,得知错误日志来源于以下代码逻辑

传输失败关键代码4

这里的代码逻辑根据注释得知,此处必须等待所有事件处理完毕,在此过程中,数据传输随时可能结束,从日志得知,waitEvents()函数返回了-1,查看此函数的逻辑

传输失败关键代码5

根据日志,在handleEvent()函数处理某个事件时返回了-1,继续看是什么事件

传输失败关键代码6

根据日志,在处理FUNCTION_SETUP事件时返回了-1,且处理该事件的handleControlRequest()函数也返回了-1,继续深入查看

传输失败关键代码7

可见,最终原因是因为收到了一条MTP_REQ_ECANCELED事件,然后系统主动取消了此次操作,猜测可能是传输文件太大触发了底层某种机制,找驱动同事协助排查,最后排查结果是因为内存越界导致的。解决办法就是驱动层将直通改为地址映射。

问题4:插入SD卡后下滑状态栏点击设置SD卡,会导致SD卡不识别

image-20231101171829124

正常点击应该进入SD卡设置页面,但这里点击会直接弹出SD卡,直接找到对应的代码frameworks/base/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java

弹出SD卡1

上图为通知栏的显示逻辑,点击通知栏会通过unmountIntent执行com.android.car.settings.storage.StorageUnmountReceiver里面的逻辑

弹出SD卡2

StorageUnmountReceiver里面执行了卸载sd的异步任务

弹出SD卡3

弹出SD卡4

可见Automotive修改了这里的逻辑,点击就是卸载SD卡,所以无需修改。

总结

通过处理这些问题,个人对Android中的MTP设计,USB设计都有了深入的了解,并且加深理解了Android架构中上层和底层之间的通信。