├── OpenInWSA
├── WSA-icon.ico
├── Enums
│ ├── ElevateFor.cs
│ └── ChoiceEnums.cs
├── OpenInWSA.csproj
├── Extensions
│ └── Extensions.cs
├── Properties
│ ├── Settings.settings
│ └── Settings.Designer.cs
├── Managers
│ ├── ElevateManager.cs
│ ├── WsaManager.cs
│ └── BrowserManager.cs
├── Classes
│ ├── Console.cs
│ ├── Browser.cs
│ └── Choices.cs
└── Program.cs
├── .gitignore
├── OpenInWSA.sln
├── .github
└── workflows
│ └── dotnet.yml
└── README.md
/OpenInWSA/WSA-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/efraimbart/OpenInWSA/HEAD/OpenInWSA/WSA-icon.ico
--------------------------------------------------------------------------------
/OpenInWSA/Enums/ElevateFor.cs:
--------------------------------------------------------------------------------
1 | namespace OpenInWSA.Enums
2 | {
3 | public enum ElevateFor
4 | {
5 | Register,
6 | Deregister
7 | }
8 | }
--------------------------------------------------------------------------------
/OpenInWSA/Enums/ChoiceEnums.cs:
--------------------------------------------------------------------------------
1 | namespace OpenInWSA.Enums
2 | {
3 | internal enum MainMenuChoices
4 | {
5 | AdbLocation,
6 | DefaultBrowser,
7 | ReRegisterAsBrowser,
8 | DeregisterAsBrowser,
9 | Exit
10 | }
11 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #Ignore thumbnails created by Windows
2 | Thumbs.db
3 | #Ignore files built by Visual Studio
4 | *.obj
5 | *.exe
6 | *.pdb
7 | *.user
8 | *.aps
9 | *.pch
10 | *.vspscc
11 | *_i.c
12 | *_p.c
13 | *.ncb
14 | *.suo
15 | *.tlb
16 | *.tlh
17 | *.bak
18 | *.cache
19 | *.ilk
20 | *.log
21 | [Bb]in
22 | [Dd]ebug*/
23 | *.lib
24 | *.sbr
25 | obj/
26 | [Rr]elease*/
27 | _ReSharper*/
28 | [Tt]est[Rr]esult*
29 | .vs/
30 | .idea/
31 | #Nuget packages folder
32 | packages/
33 |
34 |
--------------------------------------------------------------------------------
/OpenInWSA/OpenInWSA.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net6.0
6 | disable
7 | true
8 | WSA-icon.ico
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/OpenInWSA/Extensions/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Management;
4 |
5 | namespace OpenInWSA.Extensions
6 | {
7 | public static class Extensions
8 | {
9 | public static TSource ElementAtOrDefault(this IEnumerable source, int? index)
10 | {
11 | if (!index.HasValue) return default;
12 |
13 | return Enumerable.ElementAtOrDefault(source, index.Value);
14 | }
15 |
16 | public static ManagementBaseObject First(this ManagementObjectCollection source)
17 | {
18 | var results = source.GetEnumerator();
19 | results.MoveNext();
20 | return results.Current;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/OpenInWSA/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Null
7 |
8 |
9 | Null
10 |
11 |
12 | False
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/OpenInWSA.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenInWSA", "OpenInWSA\OpenInWSA.csproj", "{C9488B02-7340-450A-9CA5-D656435B919D}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | Release|Any CPU = Release|Any CPU
9 | EndGlobalSection
10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 | {C9488B02-7340-450A-9CA5-D656435B919D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 | {C9488B02-7340-450A-9CA5-D656435B919D}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 | {C9488B02-7340-450A-9CA5-D656435B919D}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 | {C9488B02-7340-450A-9CA5-D656435B919D}.Release|Any CPU.Build.0 = Release|Any CPU
15 | EndGlobalSection
16 | EndGlobal
17 |
--------------------------------------------------------------------------------
/OpenInWSA/Managers/ElevateManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using OpenInWSA.Enums;
4 |
5 | namespace OpenInWSA.Managers
6 | {
7 | public static class ElevateManager
8 | {
9 | internal static bool Elevate(ElevateFor elevateFor)
10 | {
11 | using var currentProcess = Process.GetCurrentProcess();
12 | var path = currentProcess.MainModule?.FileName;
13 |
14 | try
15 | {
16 |
17 | var startInfo = new ProcessStartInfo(path, $"/elevateFor {elevateFor}")
18 | {
19 | UseShellExecute = true,
20 | Verb = "runas"
21 | };
22 |
23 | var process = Process.Start(startInfo);
24 |
25 | if (process == null) return false;
26 |
27 | process.WaitForExit();
28 |
29 | return process.ExitCode == 0;
30 | }
31 | catch
32 | {
33 | return false;
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/OpenInWSA/Classes/Console.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace OpenInWSA.Classes
5 | {
6 | public static class Console
7 | {
8 | [DllImport("kernel32")]
9 | static extern bool AllocConsole();
10 |
11 | private static bool ConsoleAllocated { get; set; }
12 |
13 | private static void AllocateConsole()
14 | {
15 | if (ConsoleAllocated) return;
16 |
17 | AllocConsole();
18 | ConsoleAllocated = true;
19 | }
20 |
21 | //TODO: Add remaining Console methods
22 | public static void WriteLine(string value = null)
23 | {
24 | AllocateConsole();
25 | System.Console.WriteLine(value);
26 | }
27 |
28 | public static ConsoleKeyInfo ReadKey()
29 | {
30 | AllocateConsole();
31 | return System.Console.ReadKey();
32 | }
33 |
34 | public static string ReadLine()
35 | {
36 | AllocateConsole();
37 | return System.Console.ReadLine();
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/OpenInWSA/Classes/Browser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace OpenInWSA.Classes
4 | {
5 | public class Browser : IEquatable
6 | {
7 | public string Name { get; init; }
8 | public string ProgId { get; init; }
9 |
10 | public Browser()
11 | {
12 | }
13 |
14 | public Browser(string browser)
15 | {
16 | var protocolParts = browser.Split(',');
17 |
18 | Name = protocolParts[0];
19 | ProgId = protocolParts[1];
20 | }
21 |
22 | public override string ToString()
23 | {
24 | return $@"{Name},{ProgId}";
25 | }
26 |
27 | public bool Equals(Browser other) =>
28 | other is not null && (ReferenceEquals(this, other) || Name == other.Name && ProgId == other.ProgId);
29 |
30 | public override bool Equals(object obj) => obj is Browser other && Equals(other);
31 |
32 | public override int GetHashCode() => HashCode.Combine(Name, ProgId);
33 |
34 | public static bool operator ==(Browser rightBrowser, Browser leftBrowser) =>
35 | (rightBrowser is null && leftBrowser is null) || (rightBrowser?.Equals(leftBrowser) ?? false);
36 |
37 | public static bool operator !=(Browser rightBrowser, Browser leftBrowser) => !(rightBrowser == leftBrowser);
38 | }
39 | }
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 | on:
3 | push:
4 | branches: [ main ]
5 | jobs:
6 | release:
7 | runs-on: ubuntu-latest
8 | env:
9 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10 | outputs:
11 | version: ${{ steps.release.outputs.version }}
12 | tag_name: ${{ steps.release.outputs.tag_name }}
13 | steps:
14 | - name: Release
15 | id: release
16 | uses: rymndhng/release-on-push-action@v0.22.0
17 | with:
18 | bump_version_scheme: minor
19 | tag_prefix: v
20 | build:
21 | runs-on: windows-latest
22 | needs: release
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | if: needs.release.outputs.version
26 | steps:
27 | - uses: actions/checkout@v2
28 | - name: Setup .NET
29 | uses: actions/setup-dotnet@v1
30 | with:
31 | dotnet-version: 6.0.x
32 | include-prerelease: true
33 | - name: substring-action
34 | uses: bhowell2/github-substring-action@v1.0.0
35 | id: substring
36 | with:
37 | value: ${{ needs.release.outputs.version }}
38 | output_name: version
39 | index_of_str: v
40 | - name: Restore dependencies
41 | run: dotnet restore
42 | - name: Build
43 | run: dotnet publish -r win-x64 -c release -p:Version=${{ steps.substring.outputs.version }} -p:PublishSingleFile=true -p:PublishReadyToRun=true --self-contained true --framework net6.0
44 | - name: Add Release files
45 | uses: ncipollo/release-action@v1
46 | with:
47 | token: ${{ secrets.GITHUB_TOKEN }}
48 | allowUpdates: true
49 | omitBodyDuringUpdate: true
50 | omitNameDuringUpdate: true
51 | tag: ${{ needs.release.outputs.tag_name }}
52 | artifacts: OpenInWSA/bin/Release/net6.0/win-x64/publish/OpenInWSA.exe
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Open in WSA
2 |
3 | Routes clicked links through WSA (Windows Subsystem for Android™) and opens them in an applicable Android application if found, otherwise opens them in the default Windows browser.
4 |
5 | ## Getting Started
6 |
7 | Instructions to set up `Open in WSA` either using the latest release executable or via cloning the repo.
8 |
9 | ### Prerequisites
10 |
11 | Required:
12 |
13 | * [WSA](http://aka.ms/AmazonAppstore) with `Developer mode` enabled under settings.
14 | * [ADB](https://developer.android.com/studio/releases/platform-tools)
15 |
16 | Suggested:
17 |
18 | * `Subsystem resources` set to `Continuous` under WSA settings.
19 |
20 | ### Installation
21 |
22 | 1. #### Download Executable:
23 | * Download the [latest release](https://github.com/efraimbart/OpenInWSA/releases/latest) executable.
24 |
25 | or
26 |
27 | 2. #### Clone Repo
28 |
29 | * Download and set up [.Net 6.0](https://dotnet.microsoft.com/download/dotnet/6.0)
30 | * Run `$ git clone https://github.com/efraimbart/OpenInWSA`
31 | * Run `$ dotnet build`
32 |
33 |
34 | ### Setup
35 | 1. Open OpenInWSA.exe.
36 | 2. If ADB is not automatically found, set the path to ADB.
37 | 3. If the default browser is not automatically detected, set a default browser.
38 | 4. The application will attempt to elevate itself and register as a browser. If it fails, attempt to run the application manually as an administrator.
39 | 5. Set `Open In WSA` as the default for `URL:HyperText Transfer Protocol` in Windows settings under `Apps > Default apps`
40 |
41 | ## Usage
42 |
43 | Click a link from within a Windows application and it will route through WSA and open in the applicable Android application. If none are found the link will open in the default Windows browser.
44 |
45 | To route links from within Chrome through WSA install the [Open in WSA Chrome Extension](https://chrome.google.com/webstore/detail/nkfpikoflncblmlajlcagaflndiijhhl) | [Repo](https://github.com/efraimbart/OpenInWSAChromeExtension) and right click on a given link and click `Open link in WSA`.
46 |
--------------------------------------------------------------------------------
/OpenInWSA/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace OpenInWSA.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.0.2.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 |
26 | [global::System.Configuration.UserScopedSettingAttribute()]
27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
28 | public string AdbLocation {
29 | get {
30 | return ((string)(this["AdbLocation"]));
31 | }
32 | set {
33 | this["AdbLocation"] = value;
34 | }
35 | }
36 |
37 | [global::System.Configuration.UserScopedSettingAttribute()]
38 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
39 | public string DefaultBrowser {
40 | get {
41 | return ((string)(this["DefaultBrowser"]));
42 | }
43 | set {
44 | this["DefaultBrowser"] = value;
45 | }
46 | }
47 |
48 | [global::System.Configuration.UserScopedSettingAttribute()]
49 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
50 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
51 | public bool RegisteredAsBrowser {
52 | get {
53 | return ((bool)(this["RegisteredAsBrowser"]));
54 | }
55 | set {
56 | this["RegisteredAsBrowser"] = value;
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/OpenInWSA/Classes/Choices.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using OpenInWSA.Extensions;
5 |
6 | namespace OpenInWSA.Classes
7 | {
8 | public class Choices : List.IChoice>
9 | {
10 | private string Question { get; set; }
11 | private int? DefaultChoice { get; set; }
12 |
13 | public Choices(string question)
14 | {
15 | Question = question;
16 | }
17 |
18 | public Choices(string question, IEnumerable> choices) : this(question)
19 | {
20 | AddRange(choices.Select(choice => new Choice(choice)));
21 | }
22 |
23 | public Choices AddRange(IEnumerable values, Func getText) where TAdd : T
24 | {
25 | AddRange(values.Select(choice => (IChoice)new Choice(choice, getText)));
26 | return this;
27 | }
28 |
29 | public Choices Add(string text, T value, bool defaultChoice = false, bool condition = true)
30 | {
31 | if (condition)
32 | {
33 | if (defaultChoice)
34 | {
35 | DefaultChoice = Count;
36 | }
37 |
38 | Add(new Choice {Text = text, Value = value});
39 | }
40 | return this;
41 | }
42 |
43 | public Choices Add(T value, bool defaultChoice = false, bool condition = true)
44 | {
45 | return Add(value.ToString(), value, defaultChoice, condition);
46 | }
47 |
48 | public Choices Default(int? index)
49 | {
50 | DefaultChoice = index;
51 | return this;
52 | }
53 |
54 | public IChoice Choose()
55 | {
56 | var defaultChoice = this.ElementAtOrDefault(DefaultChoice);
57 | var questionWithDefault = defaultChoice != null
58 | ? $"{Question} [{defaultChoice.Text}]"
59 | : Question;
60 |
61 | Console.WriteLine(questionWithDefault);
62 |
63 | for (var i = 0; i < Count; i++)
64 | {
65 | var choice = this[i];
66 | Console.WriteLine($@"[{i + 1}] {choice.Text}");
67 | }
68 |
69 | var chosenString = Console.ReadLine();
70 | Console.WriteLine();
71 |
72 | if (string.IsNullOrWhiteSpace(chosenString))
73 | {
74 | return this.ElementAtOrDefault(DefaultChoice);
75 | }
76 |
77 | var chosen = this.FirstOrDefault(x => x.Text.Equals(chosenString, StringComparison.InvariantCultureIgnoreCase));
78 | if (chosen != null)
79 | {
80 | return chosen;
81 | }
82 |
83 | if (int.TryParse(chosenString, out var chosenNumber))
84 | {
85 | return this.ElementAtOrDefault(chosenNumber - 1);
86 | }
87 |
88 | return null;
89 | }
90 |
91 | public interface IChoice where TChoice : T
92 | {
93 | string Text { get; }
94 | TChoice Value { get; }
95 | }
96 |
97 | public class Choice : Choice
98 | {
99 | public Choice() : base()
100 | {
101 | }
102 |
103 | public Choice(KeyValuePair choice) : base(choice)
104 | {
105 | }
106 |
107 | public Choice(T choice, Func getText) : base(choice, getText)
108 | {
109 | }
110 | }
111 |
112 | public class Choice : IChoice where TChoice : T
113 | {
114 | public string Text { get; set; }
115 | public TChoice Value { get; set; }
116 |
117 | public Choice()
118 | {
119 | }
120 |
121 | public Choice(KeyValuePair choice)
122 | {
123 | (Text, Value) = choice;
124 | }
125 |
126 | public Choice(TChoice choice, Func getText)
127 | {
128 | Text = getText(choice);
129 | Value = choice;
130 | }
131 | }
132 | }
133 | }
--------------------------------------------------------------------------------
/OpenInWSA/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Management;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using OpenInWSA.Classes;
6 | using OpenInWSA.Enums;
7 | using OpenInWSA.Extensions;
8 | using OpenInWSA.Managers;
9 | using OpenInWSA.Properties;
10 | using Console = OpenInWSA.Classes.Console;
11 |
12 | CheckElevate();
13 | CheckInit();
14 |
15 | if (args.Any())
16 | {
17 | var url = args[0].Replace($"{BrowserManager.OpenInWsaProgId}://", "", StringComparison.InvariantCultureIgnoreCase);
18 |
19 | if (GetParentProcess().ProcessName == WsaManager.WsaClient)
20 | {
21 | BrowserManager.OpenInBrowser(url);
22 | }
23 | else
24 | {
25 | WsaManager.OpenInWsa(url);
26 | }
27 | }
28 | else
29 | {
30 | while (MainMenu()) { }
31 | }
32 |
33 | void CheckElevate()
34 | {
35 | if (args.Any() && args[0] == "/elevateFor")
36 | {
37 |
38 | if (!Enum.TryParse(args[1], out var elevateFor))
39 | {
40 | Environment.Exit(1);
41 | }
42 |
43 | int exitCode;
44 | switch (elevateFor)
45 | {
46 | case ElevateFor.Register:
47 | exitCode = BrowserManager.RegisterAsBrowserInner() ? 0 : 1;
48 | Environment.Exit(exitCode);
49 | break;
50 | case ElevateFor.Deregister:
51 | exitCode = BrowserManager.DeregisterAsBrowserInner() ? 0 : 1;;
52 | Environment.Exit(exitCode);
53 | break;
54 | }
55 | }
56 | }
57 |
58 | void CheckInit()
59 | {
60 | if (Settings.Default.AdbLocation == null)
61 | {
62 | if (!WsaManager.InitAdbLocation())
63 | {
64 | while (!WsaManager.UpdateAdbLocation(cancelable: false)) {}
65 | }
66 | }
67 |
68 | if (Settings.Default.DefaultBrowser == null)
69 | {
70 | if (!BrowserManager.InitDefaultBrowser())
71 | {
72 | while (!BrowserManager.UpdateDefaultBrowser(cancelable: false)) {}
73 | }
74 | }
75 |
76 | if (!Settings.Default.RegisteredAsBrowser)
77 | {
78 | if (BrowserManager.RegisterAsBrowser())
79 | {
80 | Settings.Default.RegisteredAsBrowser = true;
81 | Settings.Default.Save();
82 | }
83 | else
84 | {
85 | Console.WriteLine("Press any key to exit.");
86 | Console.ReadKey();
87 | Environment.Exit(1);
88 | }
89 | }
90 | }
91 |
92 | Process GetParentProcess()
93 | {
94 | //https://stackoverflow.com/questions/2531837/how-can-i-get-the-pid-of-the-parent-process-of-my-application/2533287#2533287
95 | var myId = Environment.ProcessId;
96 | var query = $"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {myId}";
97 | var search = new ManagementObjectSearcher("root\\CIMV2", query);
98 | var queryObj = search.Get().First();
99 | var parentId = (uint)queryObj["ParentProcessId"];
100 | var parent = Process.GetProcessById((int)parentId);
101 |
102 | return parent;
103 | }
104 |
105 | bool MainMenu()
106 | {
107 | var mainMenuChoice =
108 | new Choices(@"What would you like to do?")
109 | .Add(@"Update ADB location", MainMenuChoices.AdbLocation)
110 | .Add(@"Update default browser", MainMenuChoices.DefaultBrowser)
111 | .Add(@"Re-register as browser", MainMenuChoices.ReRegisterAsBrowser)
112 | .Add(@"Deregister as browser", MainMenuChoices.DeregisterAsBrowser)
113 | .Add("Exit", MainMenuChoices.Exit)
114 | .Choose();
115 |
116 | switch (mainMenuChoice?.Value)
117 | {
118 | case MainMenuChoices.AdbLocation:
119 | while (!WsaManager.UpdateAdbLocation(cancelable: true)) {}
120 | break;
121 | case MainMenuChoices.DefaultBrowser:
122 | while (!BrowserManager.UpdateDefaultBrowser(cancelable: true)) {}
123 | break;
124 | case MainMenuChoices.ReRegisterAsBrowser:
125 | BrowserManager.RegisterAsBrowser();
126 | break;
127 | case MainMenuChoices.DeregisterAsBrowser:
128 | BrowserManager.DeregisterAsBrowser();
129 | break;
130 | case MainMenuChoices.Exit:
131 | return false;
132 | }
133 |
134 | return true;
135 | }
136 |
--------------------------------------------------------------------------------
/OpenInWSA/Managers/WsaManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Runtime.InteropServices;
7 | using System.Text;
8 | using System.Threading;
9 | using OpenInWSA.Properties;
10 | using SharpAdbClient;
11 | using SharpAdbClient.Exceptions;
12 | using Console = OpenInWSA.Classes.Console;
13 |
14 | namespace OpenInWSA.Managers
15 | {
16 | public static class WsaManager
17 | {
18 | [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
19 | static extern bool PathFindOnPath([In, Out] StringBuilder pszFile, [In, Optional] string[] ppszOtherDirs);
20 | const int MAX_PATH = 260;
21 |
22 | internal const string WsaClient = "WsaClient";
23 |
24 | private const string Adb = @"adb";
25 | private const string ExecutableExtension = @".exe";
26 | private const string AdbExecutable = $@"{Adb}{ExecutableExtension}";
27 |
28 | private const string DeviceOffline = "device offline";
29 | private const string DeviceStillAuthorizing = "device still authorizing";
30 |
31 | private const string Host = @"127.0.0.1";
32 | private const int Port = 58526;
33 |
34 | private static readonly string HostAndPortString = $"{Host}:{Port}";
35 |
36 | private static readonly AdbServer AdbServer = new();
37 | private static readonly AdbClient AdbClient = new();
38 |
39 | internal static bool UpdateAdbLocation(bool cancelable)
40 | {
41 | var oldAdbLocation = Settings.Default.AdbLocation;
42 |
43 | var defaultValue = oldAdbLocation != null && cancelable ? $" [{oldAdbLocation}]" : "";
44 | Console.WriteLine($@"Please enter the path to ADB:{defaultValue}");
45 | var adbLocation = Console.ReadLine();
46 | Console.WriteLine();
47 |
48 | if (string.IsNullOrWhiteSpace(adbLocation) || adbLocation == oldAdbLocation) return cancelable;
49 |
50 | if (!TryValidateAdbLocation(adbLocation))
51 | {
52 | Console.WriteLine($@"Invalid ADB path ""{adbLocation}""");
53 | Console.WriteLine();
54 |
55 | return false;
56 | }
57 |
58 | Settings.Default.AdbLocation = adbLocation;
59 | Settings.Default.Save();
60 |
61 | Console.WriteLine(oldAdbLocation != null
62 | ? $@"Updated the ADB path from ""{oldAdbLocation}"" to ""{adbLocation}"""
63 | : $@"Set the ADB path to ""{adbLocation}""");
64 | Console.WriteLine();
65 |
66 | return true;
67 | }
68 |
69 | internal static bool InitAdbLocation()
70 | {
71 | if (!TryValidateAdbLocation(Adb)) return false;
72 |
73 | Settings.Default.AdbLocation = Adb;
74 | Settings.Default.Save();
75 |
76 | return true;
77 |
78 | }
79 |
80 | private static bool TryValidateAdbLocation(string baseLocation) => TryGetAdbLocation(baseLocation, out _);
81 |
82 | private static bool TryGetAdbLocation(string baseLocation, out string location)
83 | {
84 | location = ValidateAndGetAdbLocation(baseLocation);
85 | return location != null;
86 | }
87 |
88 | private static string ValidateAndGetAdbLocation(string baseLocation) => baseLocation switch
89 | {
90 | var location when File.Exists(location) => location,
91 | var location when Directory.Exists(location) =>
92 | Path.Combine(location, AdbExecutable) switch
93 | {
94 | var locationWithAdb when File.Exists(locationWithAdb) => locationWithAdb,
95 | _ => null
96 | },
97 | var location when location.EndsWith(Adb) =>
98 | new StringBuilder($"{location}{ExecutableExtension}", MAX_PATH) switch
99 | {
100 | var locationWithExe when File.Exists(locationWithExe.ToString()) => locationWithExe.ToString(),
101 | var locationWithExe when PathFindOnPath(locationWithExe) => locationWithExe.ToString(),
102 | _ => null
103 | },
104 | var location when location.EndsWith(AdbExecutable) =>
105 | new StringBuilder(location, MAX_PATH) switch
106 | {
107 | var locationSb when PathFindOnPath(locationSb) => locationSb.ToString(),
108 | _ => null
109 | },
110 | _ => null
111 | };
112 |
113 |
114 | internal static void OpenInWsa(string url)
115 | {
116 | try
117 | {
118 | var proc = Process.GetProcessesByName(WsaClient).FirstOrDefault();
119 | if (proc == null)
120 | {
121 | var info = new ProcessStartInfo(WsaClient, "/launch wsa://system")
122 | {
123 | UseShellExecute = false,
124 | CreateNoWindow = true
125 | };
126 | proc = Process.Start(info);
127 | }
128 |
129 | if (!AdbServer.GetStatus().IsRunning)
130 | {
131 | if (!TryGetAdbLocation(Settings.Default.AdbLocation, out var finalAdbLocation))
132 | {
133 | while (!UpdateAdbLocation(cancelable: false)) {}
134 | finalAdbLocation = ValidateAndGetAdbLocation(Settings.Default.AdbLocation);
135 | }
136 |
137 | AdbServer.StartServer(finalAdbLocation, restartServerIfNewer: true);
138 | }
139 |
140 | var device = AdbClient.GetDevices().FirstOrDefault(device => device.Serial == HostAndPortString);
141 | if (device == null)
142 | {
143 | do
144 | {
145 | AdbClient.Connect(new DnsEndPoint(Host, Port));
146 | Thread.Sleep(100);
147 | }
148 | while ((device = AdbClient.GetDevices().FirstOrDefault(device => device.Serial == HostAndPortString)) == null);
149 | }
150 |
151 | while (!CheckWsaViaAdb(device))
152 | {
153 | Thread.Sleep(100);
154 | }
155 |
156 | var command = $"am start -W -a android.intent.action.VIEW -d \"{url}\"";
157 |
158 | //If not async, command does not complete and application does not exit.
159 | AdbClient.ExecuteRemoteCommandAsync(command, device, new ConsoleOutputReceiver(), new CancellationToken());
160 | }
161 | catch(Exception e)
162 | {
163 | OpenInBrowser(url, $"{e}{Environment.NewLine}{Environment.NewLine}There was an issue opening the url in WSA, opening in browser instead.");
164 | }
165 | }
166 |
167 | private static bool CheckWsaViaAdb(DeviceData device)
168 | {
169 | try
170 | {
171 | AdbClient.ExecuteRemoteCommand("getprop sys.boot_completed", device, new ConsoleOutputReceiver());
172 | }
173 | catch (AdbException e)
174 | {
175 | if (e.Response.Message is DeviceOffline or DeviceStillAuthorizing)
176 | {
177 | return false;
178 | }
179 |
180 | throw;
181 | }
182 |
183 | return true;
184 | }
185 |
186 | private static void OpenInBrowser(string url, string message)
187 | {
188 | Console.WriteLine(message);
189 | Console.WriteLine();
190 |
191 | BrowserManager.OpenInBrowser(url);
192 |
193 | Console.WriteLine("Press any key to exit.");
194 | Console.ReadKey();
195 | }
196 | }
197 | }
--------------------------------------------------------------------------------
/OpenInWSA/Managers/BrowserManager.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Linq;
3 | using System.Runtime.InteropServices;
4 | using Microsoft.Win32;
5 | using OpenInWSA.Classes;
6 | using OpenInWSA.Enums;
7 | using OpenInWSA.Properties;
8 | using Console = OpenInWSA.Classes.Console;
9 |
10 | namespace OpenInWSA.Managers
11 | {
12 | public static class BrowserManager
13 | {
14 | internal const string OpenInWsaProgId = "OpenInWSAURL";
15 |
16 | private const string OpenInWsa = "Open In WSA";
17 |
18 | internal static bool UpdateDefaultBrowser(bool cancelable)
19 | {
20 | //TODO: Merge with LocalUser?
21 | using var registeredApplications = Registry.LocalMachine.OpenSubKey(@"Software\RegisteredApplications");
22 |
23 | //TODO: This will throw an exception if it can't find RegisteredApplications, URLAssociations or http
24 | var browsers = registeredApplications
25 | .GetValueNames()
26 | .Where(name => name != OpenInWsa)
27 | .Select(name => registeredApplications.GetValue(name) as string)
28 | .Where(value => value != null && value.StartsWith(@"Software\Clients\StartMenuInternet\"))
29 | .Select(value =>
30 | {
31 | using var urlAssociationsKey = Registry.LocalMachine.OpenSubKey($@"{value}\URLAssociations");
32 | var progId = urlAssociationsKey.GetValue("http").ToString();
33 |
34 | return GetBrowserFromProgId(progId);
35 | }).ToList();
36 |
37 | var oldDefaultBrowser = Settings.Default.DefaultBrowser != null
38 | ? new Browser(Settings.Default.DefaultBrowser)
39 | : null;
40 | var oldDefaultBrowserIndex = Settings.Default.DefaultBrowser != null && cancelable
41 | ? browsers.IndexOf(oldDefaultBrowser)
42 | : (int?)null;
43 |
44 | var defaultBrowserChoice = new Choices