Skip to content

Commit 5a0c751

Browse files
authored
Access android (MTP) devices (#926)
1 parent 8e0d57d commit 5a0c751

25 files changed

+937
-441
lines changed

Common/Extensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34

45
namespace Files.Common
56
{
67
public static class Extensions
78
{
9+
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(
10+
this IEnumerable<TSource> source,
11+
IEnumerable<TSource> other,
12+
Func<TSource, TKey> keySelector)
13+
{
14+
var set = new HashSet<TKey>(other.Select(keySelector));
15+
foreach (var item in source)
16+
if (set.Add(keySelector(item)))
17+
yield return item;
18+
}
19+
820
public static TOut Get<TOut, TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TOut defaultValue = default(TOut))
921
{
1022
// If setting doesn't exist, create it.

Files.Launcher/Program.cs

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Linq;
99
using System.Runtime.InteropServices;
10+
using System.Text.RegularExpressions;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Vanara.Windows.Shell;
@@ -339,7 +340,7 @@ private static void HandleApplicationsLaunch(IEnumerable<string> applications, A
339340
}
340341
}
341342

342-
private static void HandleApplicationLaunch(string application, AppServiceRequestReceivedEventArgs args)
343+
private static async void HandleApplicationLaunch(string application, AppServiceRequestReceivedEventArgs args)
343344
{
344345
var arguments = args.Request.Message.Get("Arguments", "");
345346
var workingDirectory = args.Request.Message.Get("WorkingDirectory", "");
@@ -395,43 +396,46 @@ private static void HandleApplicationLaunch(string application, AppServiceReques
395396
{
396397
try
397398
{
398-
var split = application.Split(';').Where(x => !string.IsNullOrWhiteSpace(x));
399-
if (split.Count() == 1)
399+
await Win32API.StartSTATask(() =>
400400
{
401-
Process.Start(application);
402-
}
403-
else
404-
{
405-
var groups = split.GroupBy(x => new
401+
var split = application.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => GetMtpPath(x));
402+
if (split.Count() == 1)
406403
{
407-
Dir = Path.GetDirectoryName(x),
408-
Prog = Win32API.GetFileAssociation(x).Result ?? Path.GetExtension(x)
409-
});
410-
foreach (var group in groups)
404+
Process.Start(split.First());
405+
}
406+
else
411407
{
412-
if (!group.Any()) continue;
413-
var files = group.Select(x => new ShellItem(x));
414-
using var sf = files.First().Parent;
415-
IContextMenu menu = null;
416-
try
408+
var groups = split.GroupBy(x => new {
409+
Dir = Path.GetDirectoryName(x),
410+
Prog = Win32API.GetFileAssociation(x).Result ?? Path.GetExtension(x)
411+
});
412+
foreach (var group in groups)
417413
{
418-
menu = sf.GetChildrenUIObjects<IContextMenu>(null, files.ToArray());
419-
menu.QueryContextMenu(Vanara.PInvoke.HMENU.NULL, 0, 0, 0, CMF.CMF_DEFAULTONLY);
420-
var pici = new CMINVOKECOMMANDINFOEX();
421-
pici.lpVerb = CMDSTR_OPEN;
422-
pici.nShow = Vanara.PInvoke.ShowWindowCommand.SW_SHOW;
423-
pici.cbSize = (uint)Marshal.SizeOf(pici);
424-
menu.InvokeCommand(pici);
425-
}
426-
finally
427-
{
428-
foreach (var elem in files)
429-
elem.Dispose();
430-
if (menu != null)
431-
Marshal.ReleaseComObject(menu);
414+
if (!group.Any()) continue;
415+
var files = group.Select(x => new ShellItem(x));
416+
using var sf = files.First().Parent;
417+
Vanara.PInvoke.Shell32.IContextMenu menu = null;
418+
try
419+
{
420+
menu = sf.GetChildrenUIObjects<Vanara.PInvoke.Shell32.IContextMenu>(null, files.ToArray());
421+
menu.QueryContextMenu(Vanara.PInvoke.HMENU.NULL, 0, 0, 0, Vanara.PInvoke.Shell32.CMF.CMF_DEFAULTONLY);
422+
var pici = new Vanara.PInvoke.Shell32.CMINVOKECOMMANDINFOEX();
423+
pici.lpVerb = Vanara.PInvoke.Shell32.CMDSTR_OPEN;
424+
pici.nShow = Vanara.PInvoke.ShowWindowCommand.SW_SHOW;
425+
pici.cbSize = (uint)Marshal.SizeOf(pici);
426+
menu.InvokeCommand(pici);
427+
}
428+
finally
429+
{
430+
foreach (var elem in files)
431+
elem.Dispose();
432+
if (menu != null)
433+
Marshal.ReleaseComObject(menu);
434+
}
432435
}
433436
}
434-
}
437+
return true;
438+
});
435439
}
436440
catch (Win32Exception)
437441
{
@@ -482,6 +486,19 @@ private static bool HandleCommandLineArgs()
482486
return false;
483487
}
484488

489+
private static string GetMtpPath(string executable)
490+
{
491+
if (executable.StartsWith("\\\\?\\"))
492+
{
493+
using var computer = new ShellFolder(Vanara.PInvoke.Shell32.KNOWNFOLDERID.FOLDERID_ComputerFolder);
494+
using var device = computer.FirstOrDefault(i => executable.Replace("\\\\?\\", "").StartsWith(i.Name));
495+
var deviceId = device?.ParsingName;
496+
var itemPath = Regex.Replace(executable, @"^\\\\\?\\[^\\]*\\?", "");
497+
return deviceId != null ? Path.Combine(deviceId, itemPath) : executable;
498+
}
499+
return executable;
500+
}
501+
485502
private static void Connection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
486503
{
487504
// Signal the event so the process can shut down

Files/BaseLayout.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
205205
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
206206
}
207207

208-
protected override void OnNavigatedTo(NavigationEventArgs eventArgs)
208+
protected override async void OnNavigatedTo(NavigationEventArgs eventArgs)
209209
{
210210
base.OnNavigatedTo(eventArgs);
211211
// Add item jumping handler
@@ -221,7 +221,7 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs)
221221
App.CurrentInstance.NavigationToolbar.CanRefresh = true;
222222
IsItemSelected = false;
223223
AssociatedViewModel.IsFolderEmptyTextDisplayed = false;
224-
App.CurrentInstance.FilesystemViewModel.WorkingDirectory = parameters;
224+
await App.CurrentInstance.FilesystemViewModel.SetWorkingDirectory(parameters);
225225

226226
// pathRoot will be empty on recycle bin path
227227
string pathRoot = Path.GetPathRoot(App.CurrentInstance.FilesystemViewModel.WorkingDirectory);
@@ -234,8 +234,10 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs)
234234
App.CurrentInstance.NavigationToolbar.CanNavigateToParent = true;
235235
}
236236
App.CurrentInstance.InstanceViewModel.IsPageTypeNotHome = true; // show controls that were hidden on the home page
237-
App.CurrentInstance.InstanceViewModel.IsPageTypeNotRecycleBin =
238-
!App.CurrentInstance.FilesystemViewModel.WorkingDirectory.StartsWith(AppSettings.RecycleBinPath);
237+
App.CurrentInstance.InstanceViewModel.IsPageTypeRecycleBin =
238+
App.CurrentInstance.FilesystemViewModel.WorkingDirectory.StartsWith(App.AppSettings.RecycleBinPath);
239+
App.CurrentInstance.InstanceViewModel.IsPageTypeMtpDevice =
240+
App.CurrentInstance.FilesystemViewModel.WorkingDirectory.StartsWith("\\\\?\\");
239241

240242
App.CurrentInstance.FilesystemViewModel.RefreshItems();
241243

@@ -468,9 +470,9 @@ protected async void Item_DragStarting(object sender, DragStartingEventArgs e)
468470
foreach (ListedItem item in App.CurrentInstance.ContentPage.SelectedItems)
469471
{
470472
if (item.PrimaryItemAttribute == StorageItemTypes.File)
471-
selectedStorageItems.Add(await StorageFile.GetFileFromPathAsync(item.ItemPath));
473+
selectedStorageItems.Add(await ItemViewModel.GetFileFromPathAsync(item.ItemPath));
472474
else if (item.PrimaryItemAttribute == StorageItemTypes.Folder)
473-
selectedStorageItems.Add(await StorageFolder.GetFolderFromPathAsync(item.ItemPath));
475+
selectedStorageItems.Add(await ItemViewModel.GetFolderFromPathAsync(item.ItemPath));
474476
}
475477

476478
if (selectedStorageItems.Count == 0)

Files/DataModels/SidebarPinnedModel.cs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.IO;
88
using System.Linq;
9+
using System.Threading.Tasks;
910
using Windows.Storage;
1011

1112
namespace Files.DataModels
@@ -35,12 +36,12 @@ public List<string> GetItems()
3536
return Items;
3637
}
3738

38-
public void AddItem(string item)
39+
public async void AddItem(string item)
3940
{
4041
if (!Items.Contains(item))
4142
{
4243
Items.Add(item);
43-
AddItemToSidebar(item);
44+
await AddItemToSidebar(item);
4445
Save();
4546
}
4647
}
@@ -65,12 +66,13 @@ public void Save()
6566
}
6667
}
6768

68-
public async void AddItemToSidebar(string path)
69+
public async Task AddItemToSidebar(string path)
6970
{
7071
try
7172
{
72-
StorageFolder folder = await StorageFolder.GetFolderFromPathAsync(path);
73-
int insertIndex = App.sideBarItems.IndexOf(App.sideBarItems.Last(x => x.ItemType == NavigationControlItemType.Location
73+
var item = await DrivesManager.GetRootFromPath(path);
74+
StorageFolder folder = await StorageFileExtensions.GetFolderFromPathAsync(path, item);
75+
int insertIndex = App.sideBarItems.IndexOf(App.sideBarItems.Last(x => x.ItemType == NavigationControlItemType.Location
7476
&& !x.Path.Equals(App.AppSettings.RecycleBinPath))) + 1;
7577
var locationItem = new LocationItem
7678
{
@@ -85,29 +87,23 @@ public async void AddItemToSidebar(string path)
8587
{
8688
Debug.WriteLine(ex.Message);
8789
}
88-
catch (ArgumentException ex)
90+
catch (Exception ex) when (
91+
ex is ArgumentException // Pinned item was invalid
92+
|| ex is FileNotFoundException // Pinned item was deleted
93+
|| ex is System.Runtime.InteropServices.COMException // Pinned item's drive was ejected
94+
|| (uint)ex.HResult == 0x800700A1) // The specified path is invalid (usually an mtp device was disconnected)
8995
{
9096
Debug.WriteLine("Pinned item was invalid and will be removed from the file lines list soon: " + ex.Message);
9197
RemoveItem(path);
9298
}
93-
catch (FileNotFoundException ex)
94-
{
95-
Debug.WriteLine("Pinned item was deleted and will be removed from the file lines list soon: " + ex.Message);
96-
RemoveItem(path);
97-
}
98-
catch (System.Runtime.InteropServices.COMException ex)
99-
{
100-
Debug.WriteLine("Pinned item's drive was ejected and will be removed from the file lines list soon: " + ex.Message);
101-
RemoveItem(path);
102-
}
10399
}
104100

105-
public void AddAllItemsToSidebar()
101+
public async void AddAllItemsToSidebar()
106102
{
107103
for (int i = 0; i < Items.Count(); i++)
108104
{
109105
string path = Items[i];
110-
AddItemToSidebar(path);
106+
await AddItemToSidebar(path);
111107
}
112108
}
113109

Files/Dialogs/AddItemDialog.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static async void CreateFile(AddItemType fileType)
5353
{
5454
currentPath = TabInstance.FilesystemViewModel.WorkingDirectory;
5555
}
56-
StorageFolder folderToCreateItem = await StorageFolder.GetFolderFromPathAsync(currentPath);
56+
StorageFolder folderToCreateItem = await Filesystem.ItemViewModel.GetFolderFromPathAsync(currentPath);
5757
RenameDialog renameDialog = new RenameDialog();
5858

5959
var renameResult = await renameDialog.ShowAsync();

Files/Files.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@
162162
<Compile Include="Dialogs\ConfirmDeleteDialog.xaml.cs">
163163
<DependentUpon>ConfirmDeleteDialog.xaml</DependentUpon>
164164
</Compile>
165+
<Compile Include="Filesystem\StorageFileHelpers\StorageFileExtensions.cs" />
166+
<Compile Include="Filesystem\StorageFileHelpers\IStorageItemWithPath.cs" />
167+
<Compile Include="Filesystem\StorageFileHelpers\StorageFileWithPath.cs" />
168+
<Compile Include="Filesystem\StorageFileHelpers\StorageFolderWithPath.cs" />
165169
<Compile Include="Helpers\AcrylicTheme.cs" />
166170
<Compile Include="Helpers\DialogDisplayHelper.cs" />
167171
<Compile Include="Helpers\DispatcherHelper.cs" />

Files/Filesystem/DriveItem.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class DriveItem : INavigationControlItem
1212
public string Glyph { get; set; }
1313
public string Text { get; set; }
1414
public string Path { get; set; }
15+
public StorageFolder Root { get; set; }
1516
public NavigationControlItemType ItemType { get; set; } = NavigationControlItemType.Drive;
1617
public ByteSize MaxSpace { get; set; }
1718
public ByteSize FreeSpace { get; set; }
@@ -40,7 +41,8 @@ public DriveItem(StorageFolder root, DriveType type)
4041
{
4142
Text = root.DisplayName;
4243
Type = type;
43-
Path = root.Path;
44+
Path = string.IsNullOrEmpty(root.Path) ? $"\\\\?\\{root.Name}\\" : root.Path;
45+
Root = root;
4446

4547
var properties = Task.Run(async () =>
4648
{

Files/Filesystem/Drives.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private async void DeviceAdded(DeviceWatcher sender, DeviceInformation args)
121121
}
122122

123123
// If drive already in list, skip.
124-
if (Drives.Any(x => x.Path == root.Name))
124+
if (Drives.Any(x => string.IsNullOrEmpty(root.Path) ? x.Path.Contains(root.Name) : x.Path == root.Path))
125125
{
126126
return;
127127
}
@@ -288,6 +288,52 @@ private void GetVirtualDrivesList(IList<DriveItem> list)
288288
}
289289
}
290290

291+
public static async Task<StorageFolderWithPath> GetRootFromPath(string devicePath)
292+
{
293+
if (!Path.IsPathRooted(devicePath))
294+
{
295+
return null;
296+
}
297+
var rootPath = Path.GetPathRoot(devicePath);
298+
if (devicePath.StartsWith("\\\\?\\"))
299+
{
300+
// Check among already discovered drives
301+
StorageFolder matchingDrive = App.AppSettings.DrivesManager.Drives.FirstOrDefault(x =>
302+
InstanceTabsView.NormalizePath(x.Path) == InstanceTabsView.NormalizePath(rootPath))?.Root;
303+
if (matchingDrive == null)
304+
{
305+
// Check on all removable drives
306+
var remDevices = await DeviceInformation.FindAllAsync(StorageDevice.GetDeviceSelector());
307+
foreach (var item in remDevices)
308+
{
309+
try
310+
{
311+
var root = StorageDevice.FromId(item.Id);
312+
if (InstanceTabsView.NormalizePath(rootPath).Replace("\\\\?\\", "") == root.Name.ToUpperInvariant())
313+
{
314+
matchingDrive = root;
315+
break;
316+
}
317+
}
318+
catch (Exception)
319+
{
320+
// Ignore this..
321+
}
322+
}
323+
}
324+
if (matchingDrive != null)
325+
{
326+
return new StorageFolderWithPath(matchingDrive, rootPath);
327+
}
328+
}
329+
// It's ok to return null here, on normal drives StorageFolder.GetFolderFromPathAsync works
330+
//else
331+
//{
332+
// return new StorageFolderWithPath(await StorageFolder.GetFolderFromPathAsync(rootPath), rootPath);
333+
//}
334+
return null;
335+
}
336+
291337
public void Dispose()
292338
{
293339
if (_deviceWatcher.Status == DeviceWatcherStatus.Started || _deviceWatcher.Status == DeviceWatcherStatus.EnumerationCompleted)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Windows.Storage;
2+
3+
namespace Files.Filesystem
4+
{
5+
public interface IStorageItemWithPath
6+
{
7+
public string Path { get; set; }
8+
public IStorageItem Item { get; set; }
9+
}
10+
}

0 commit comments

Comments
 (0)