Reputation: 163
I am modifying an existing application, written in C# in Xamarin, built exclusively for Android.
The existing app searches for files on a wifi card in a camera, and downloads them via an HTTP interface. I'm trying to get it to download the files using tethered with a USB cable instead, but can't get the files to download.
I'm using MtpDevice to try and download, the but the ImportFile function always fails. Unfortunately, it never throws an exception or gives any sort of useful information as to why.
The code below shows pretty much what I'm doing, although I have removed a bunch of stuff that is not relevant to the problem.
As I am modifying an existing application, the code is a little convoluted, as there is one method to find the files, then a background thread calls into another method to do the actual download, so the user can continue working while the files are downloaded.
Also, some of the code I have shown in GetFileListAsync is actually in other methods as they are used multiple times... I have not included the permissions stuff, as that all appears to be working fine.
This is the method that gets called first, to find a list of object handles.
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;
}
I know it's possible to get all the handles at once rather than recursing through folders, but for now I'm doing it as the old code did that.
The list of MyFileClass objects are added to a SQLite database, then the background thread de-queues them one at a time and calls DownloadFileAsync to get each file. This method uses the same device as used in the GetFileListAsync method, and it also checks the permission is still available.
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;
}
I can't find a single example showing this working. I've seen one post on here that says the file has to be imported to the external cache folder, and one that says the second parameter should include the filename, but neither of those work.
I've been pulling out my hair over this - help!!
Upvotes: 1
Views: 294
Reputation: 163
So, thanks to SushiHangover both for pointing me at the problem and for opening my eyes to the delights of logcat. The answer is that the statement
the file has to be imported to the external cache folder
is absolutely true - but it has to actually be external.
GetExternalCacheDirs() actually returns a folder even if you don't have a physical external media, which seems crazy to me, but there you are.
Incidentally, it is also true that the destination path must include the filename. The documentation says:
destPath String: path to destination for the file transfer. This path should be in the external storage as defined by Environment.getExternalStorageDirectory() This value must never be null.
To me, this isn't clear at all.
Upvotes: 1