├── 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($@"Please select a default browser:") 45 | .AddRange(browsers, browser => browser.Name) 46 | .Add("Cancel", condition: cancelable) 47 | .Default(oldDefaultBrowserIndex) 48 | .Choose(); 49 | 50 | if (defaultBrowserChoice == null) 51 | { 52 | Console.WriteLine("Bad selection"); 53 | Console.WriteLine(); 54 | 55 | return false; 56 | } 57 | 58 | switch (defaultBrowserChoice.Value) 59 | { 60 | case "Cancel": 61 | case Browser defaultBrowser when defaultBrowser == oldDefaultBrowser: 62 | return cancelable; 63 | case Browser defaultBrowser: 64 | Settings.Default.DefaultBrowser = defaultBrowser.ToString(); 65 | Settings.Default.Save(); 66 | 67 | Console.WriteLine(oldDefaultBrowser != null 68 | ? $@"Updated the the default browser from ""{oldDefaultBrowser.Name}"" to ""{defaultBrowser.Name}""" 69 | : $@"Set the the default browser to ""{defaultBrowser.Name}"""); 70 | Console.WriteLine(); 71 | 72 | return true; 73 | default: 74 | return false; 75 | } 76 | } 77 | 78 | internal static bool RegisterAsBrowser() 79 | { 80 | Console.WriteLine(@"Registering as browser"); 81 | 82 | if (RegisterAsBrowserInner() || ElevateManager.Elevate(ElevateFor.Register)) 83 | { 84 | Console.WriteLine(@"Successfully registered as browser."); 85 | Console.WriteLine(); 86 | 87 | return true; 88 | } 89 | else 90 | { 91 | Console.WriteLine(@"Failed to register as browser. To register as browser, please attempt to run the application manually as administrator."); 92 | Console.WriteLine(); 93 | 94 | return false; 95 | } 96 | } 97 | 98 | internal static bool RegisterAsBrowserInner() 99 | { 100 | using var currentProcess = Process.GetCurrentProcess(); 101 | var path = currentProcess.MainModule?.FileName; 102 | 103 | try 104 | { 105 | using var openInWsaKey = Registry.LocalMachine.CreateSubKey($@"software\Clients\StartMenuInternet\{OpenInWsa}"); 106 | 107 | using var capabilitiesKey = openInWsaKey.CreateSubKey("Capabilities"); 108 | capabilitiesKey.SetValue("ApplicationDescription", OpenInWsa); 109 | capabilitiesKey.SetValue("ApplicationIcon", $"\"{path}\",0"); 110 | capabilitiesKey.SetValue("ApplicationName", OpenInWsa); 111 | 112 | using var urlAssociationsKey = capabilitiesKey.CreateSubKey("URLAssociations"); 113 | urlAssociationsKey.SetValue("http", OpenInWsaProgId); 114 | urlAssociationsKey.SetValue("https", OpenInWsaProgId); 115 | 116 | using var defaultIconKey = openInWsaKey.CreateSubKey("DefaultIcon"); 117 | defaultIconKey.SetValue("", $"\"{path}\",0"); 118 | 119 | using var commandKey = openInWsaKey.CreateSubKey(@"shell\open\command"); 120 | commandKey.SetValue("", $"\"{path}\""); 121 | 122 | using var openInWsaUrlKey = Registry.ClassesRoot.CreateSubKey(OpenInWsaProgId); 123 | openInWsaUrlKey.SetValue("", OpenInWsa); 124 | openInWsaUrlKey.SetValue("EditFlags", 0x2); //TODO: Find out if needed 125 | openInWsaUrlKey.SetValue("FriendlyTypeName", OpenInWsa); //TODO: Find out if needed 126 | openInWsaUrlKey.SetValue("", $"URL:{OpenInWsa} Protocol"); 127 | openInWsaUrlKey.SetValue("URL Protocol", ""); 128 | 129 | using var urlApplicationKey = openInWsaUrlKey.CreateSubKey("Application"); 130 | urlApplicationKey.SetValue("ApplicationDescription", OpenInWsa); 131 | urlApplicationKey.SetValue("ApplicationIcon", $"\"{path}\",0"); 132 | urlApplicationKey.SetValue("ApplicationName", OpenInWsa); 133 | 134 | using var urlDefaultIconKey = openInWsaUrlKey.CreateSubKey("DefaultIcon"); 135 | defaultIconKey.SetValue("", $"\"{path}\",0"); 136 | 137 | using var urlCommandKey = openInWsaUrlKey.CreateSubKey(@"shell\open\command"); 138 | urlCommandKey.SetValue("", $"\"{path}\" \"%1\""); 139 | 140 | using var registeredApplications = Registry.LocalMachine.CreateSubKey(@"Software\RegisteredApplications"); 141 | registeredApplications.SetValue(OpenInWsa, $@"Software\Clients\StartMenuInternet\{OpenInWsa}\Capabilities"); 142 | } 143 | catch 144 | { 145 | return false; 146 | } 147 | 148 | return true; 149 | } 150 | 151 | internal static bool DeregisterAsBrowser() 152 | { 153 | Console.WriteLine(@"Deregistering as browser"); 154 | 155 | if (DeregisterAsBrowserInner() || ElevateManager.Elevate(ElevateFor.Deregister)) 156 | { 157 | Console.WriteLine(@"Successfully deregistered as browser."); 158 | Console.WriteLine(); 159 | 160 | return true; 161 | } 162 | else 163 | { 164 | Console.WriteLine(@"Failed to deregister as browser. To deregister as browser, please attempt to run the application manually as administrator."); 165 | Console.WriteLine(); 166 | 167 | return false; 168 | } 169 | } 170 | 171 | internal static bool DeregisterAsBrowserInner() 172 | { 173 | try 174 | { 175 | Registry.LocalMachine.DeleteSubKeyTree($@"software\Clients\StartMenuInternet\{OpenInWsa}", throwOnMissingSubKey: false); 176 | Registry.ClassesRoot.DeleteSubKeyTree(OpenInWsaProgId, throwOnMissingSubKey: false); 177 | 178 | using var registeredApplications = Registry.LocalMachine.CreateSubKey(@"Software\RegisteredApplications"); 179 | registeredApplications.DeleteValue(OpenInWsa, throwOnMissingValue: false); 180 | } 181 | catch 182 | { 183 | return false; 184 | } 185 | 186 | return true; 187 | } 188 | 189 | internal static bool InitDefaultBrowser() 190 | { 191 | using var userChoiceKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"); 192 | 193 | //TODO: This will throw an exception if it can't find UserChoice, Progid 194 | var progId = userChoiceKey.GetValue("Progid").ToString(); 195 | 196 | if (progId == OpenInWsaProgId) return false; 197 | 198 | var currentDefaultBrowser = GetBrowserFromProgId(progId); 199 | 200 | Settings.Default.DefaultBrowser = currentDefaultBrowser.ToString(); 201 | Settings.Default.Save(); 202 | 203 | return true; 204 | } 205 | 206 | private static Browser GetBrowserFromProgId(string progId) { 207 | //TODO: This will throw an exception if it can't find progId class 208 | using var progKey = Registry.ClassesRoot.OpenSubKey(progId); 209 | using var applicationNameKey = progKey.OpenSubKey("Application"); 210 | var applicationName = applicationNameKey?.GetValue("ApplicationName").ToString() ?? progId; 211 | 212 | return new Browser 213 | { 214 | Name = applicationName, 215 | ProgId = progId 216 | }; 217 | } 218 | 219 | private static bool TryGetCommandFromBrowser(Browser browser, out string command) 220 | { 221 | using var progKey = Registry.ClassesRoot.OpenSubKey(browser.ProgId); 222 | 223 | if (progKey == null) 224 | { 225 | command = null; 226 | return false; 227 | } 228 | 229 | //TODO: This will throw an exception if it can't find command for the progId 230 | using var commandKey = progKey.OpenSubKey(@"shell\open\command"); 231 | command = commandKey.GetValue(null).ToString(); 232 | 233 | return true; 234 | } 235 | 236 | 237 | internal static void OpenInBrowser(string url) 238 | { 239 | var browser = new Browser(Settings.Default.DefaultBrowser); 240 | 241 | if (!TryGetCommandFromBrowser(browser, out var command)) 242 | { 243 | while (!UpdateDefaultBrowser(cancelable: false)) { } 244 | 245 | OpenInBrowser(url); 246 | return; 247 | } 248 | 249 | command = command.Replace("%1", url); 250 | var info = new ProcessStartInfo("cmd", $@"/c ""{command}""") 251 | { 252 | UseShellExecute = false, 253 | CreateNoWindow = true 254 | }; 255 | var proc = Process.Start(info); 256 | } 257 | } 258 | } --------------------------------------------------------------------------------