首页 > 解决方案 > 为什么 MtpDevice.ImportFile 总是失败?

问题描述

我正在修改一个现有的应用程序,在 Xamarin 中用 C# 编写,专为 Android 构建。

现有应用程序在相机中搜索 wifi 卡上的文件,并通过 HTTP 接口下载它们。我试图让它使用与 USB 电缆连接的文件来下载文件,但无法下载文件。

我正在使用 MtpDevice 尝试下载,但 ImportFile 函数总是失败。不幸的是,它从不抛出异常或提供任何关于原因的有用信息。

下面的代码几乎显示了我正在做的事情,尽管我删除了一堆与问题无关的东西。

当我正在修改现有应用程序时,代码有点复杂,因为有一种方法可以找到文件,然后后台线程调用另一种方法来进行实际下载,因此用户可以在下载文件的同时继续工作.

此外,我在 GetFileListAsync 中显示的一些代码实际上是在其他方法中,因为它们被多次使用......我没有包括权限的东西,因为一切似乎都工作正常。

这是首先调用的方法,用于查找对象句柄列表。

public async Task<List<MyFileClass>> GetFileListAsync()
{
    var results = new List<MyFileClass>();
    UsbDeviceConnection usbDeviceConnection = null;
    MtpDevice mtpDevice = null;
    UsbDevice usbDevice = null;

    // all this stuff works as expected...

    UsbManager usbManager = (UsbManager)Android.App.Application.Context.GetSystemService(Context.UsbService);

    try
    {
        if (usbManager.DeviceList != null && usbManager.DeviceList.Count > 0)
        {
            foreach (var usbAccessory in usbManager.DeviceList)
            {
                var device = usbAccessory.Value;
                usbDevice = usbAccessory.Value;
                break;  // should only ever be one, but break here anyway
            }
        }
    }
    catch (Exception ex)
    {
        Log.Error(ex, "Error getting USB devices");
    }

    if (usbDevice == null)
    {
        Log.Information("ConnectedDevice is null");
        return false;
    }

    if (!UsbManager.HasPermission(usbDevice))
    {
        Log.Information("Requesting permission must have failed");
        return false;
    }

    try
    {
        usbDeviceConnection = UsbManager.OpenDevice(usbDevice);
    }
    catch (Exception ex)
    {
        Log.Error(ex, "opening usb connection");
        return false;
    }

    try
    {
        mtpDevice = new MtpDevice(usbDevice);
    }
    catch (Exception ex)
    {
        Log.Error(ex, "creating mtpdevice");
        return false;
    }

    try
    {
        mtpDevice.Open(usbDeviceConnection);
    }
    catch (Exception ex)
    {
        Log.Error(ex, " opening mtpdevice");
        usbDeviceConnection.Close();
        return false;
    }

    // then start looking for files

    var storageUnits = mtpDevice.GetStorageIds();
    if (storageUnits == null || storageUnits.Length == 0)
    {
        Log.Information("StorageUnits is empty");
        mtpDevice.Close();
        usbDeviceConnection.Close();
        return false;
    }

    foreach (var storageUnitId in storageUnits)
    {
        var storageUnit = mtpDevice.GetStorageInfo(storageUnitId);
        if (storageUnit != null)
        {
            // recurse directories to get list of files
            await RecurseMTPDirectories(results, storageUnitId, docketId, docket, db);
        }
    }
}

private async Task RecurseMTPDirectories(List<MyFileClass> files, int storageUnitId, string parentFolder = "\\", int parentHandle = -1)
{
    var results = new List<FlashAirFile>();
    var directoryObjects = new List<MtpObjectInfo>();
    var objectHandles = mtpDevice.GetObjectHandles(storageUnitId, 0, parentHandle);

    if (objectHandles != null && objectHandles.Length > 0)
    {
        foreach (var objectHandle in objectHandles)
        {
            MtpObjectInfo objectInfo = mtpDevice.GetObjectInfo(objectHandle);
            if (objectInfo != null)
            {
                if (objectInfo.Format == MtpFormat.Association)
                {
                    // add to the list to recurse into, but do this at the end
                    directoryObjects.Add(objectInfo);
                }
                else
                {
                    // it' a file - add it only if it's one we want
                    try
                    {
                        var file = new MyFileClass()
                        {
                            TotalBytes = objectInfo.CompressedSize,
                            FileNameOnCard = objectInfo.Name,
                            DirectoryOnCard = parentFolder,
                            MTPHandle = objectHandle
                        };
                    }
                    catch (Exception e)
                    {
                        Log.Error(e, " trying to create MTP FlashAirFile");
                    }
                }
            }
        }
    }

    foreach (var directoryObject in directoryObjects)
    {
        string fullname = parentFolder + directoryObject.Name;
        await RecurseMTPDirectories(files, storageUnitId, $"{fullname}\\", directoryObject.ObjectHandle);
    }
    return results;
}

我知道一次获取所有句柄而不是通过文件夹递归是可能的,但现在我正在按照旧代码那样做。

MyFileClass 对象列表被添加到 SQLite 数据库中,然后后台线程一次将它们出列并调用 DownloadFileAsync 来获取每个文件。此方法使用与 GetFileListAsync 方法中使用的设备相同的设备,并且它还检查权限是否仍然可用。

public async Task<int> DownloadFileAsync(MyFileClass file, string destination)
{
    int receivedBytes = 0;
    int objectHandle = file.MTPHandle;

    connectedDevice = await GetAttachedDevice();
    if (connectedDevice == null || !UsbManager.HasPermission(connectedDevice))
        return receivedBytes;

    if (!await OpenAttachedDevice())
        return receivedBytes;

    var rootFolder = await FileSystem.Current.GetFolderFromPathAsync(destination);
    var localFile = rootFolder.Path;

    try
    {
    Log.Information($"Attempting to download ID {objectHandle} to {localFile}");

        // try downloading just using path
        bool success = mtpDevice.ImportFile(objectHandle, localFile);
        if (!success)
        {
            // try it with a / on the end of the path
            localFile += '/';
            Log.Information($"Attempting to download ID {file.DownloadManagerId} to {localFile}");
            success = mtpDevice.ImportFile(objectHandle, localFile);
        }
        if (!success)
        {
            // try it with the filename on the end of the path as well
            localFile += file.FileNameOnSdCard;
            Log.Information($"Attempting to download ID {file.DownloadManagerId} to {localFile}");
            success = mtpDevice.ImportFile(objectHandle, localFile);
        }


        if (!success)
        {
            throw new Exception($"mtpDevice.ImportFile failed for {file.FileNameOnSdCard}");
        }

        // do stuff here to handle success
    }
    catch (Exception ex) when (ex is OperationCanceledException || ex is TaskCanceledException)
    {
        // do some other stuff here in the database

        //rethrow the exception so it can be handled further up the chain.
        throw;
    }

    return receivedBytes;
}

我找不到显示此工作的单个示例。我在这里看到一篇文章说必须将文件导入外部缓存文​​件夹,还有一篇说第二个参数应该包含文件名,但这些都不起作用。

我一直在为此拔头发-救命!!

标签: c#androidxamarinimportmtp

解决方案


因此,感谢 SushiHangover 指出问题并让我看到 logcat 的乐趣。答案是声明

该文件必须导入到外部缓存文​​件夹

是绝对正确的——但它实际上必须是外部的。

即使您没有物理外部媒体,GetExternalCacheDirs() 实际上也会返回一个文件夹,这对我来说似乎很疯狂,但您就是这样。

顺便说一句,目标路径也必须包含文件名。文档说:

destPath String : 文件传输目的地的路径。此路径应位于 Environment.getExternalStorageDirectory() 定义的外部存储中。此值不得为空。

对我来说,这根本不清楚。


推荐阅读