diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 5dc4b579..a8c5d798 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -65,6 +65,7 @@ body: - ".NET 6" - ".NET 7" - ".NET 8" + - ".NET 9" - "Others" - type: dropdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..91a2e8ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Question + url: https://github.com/SharpAdb/AdvancedSharpAdbClient/discussions/new?category=q-a + about: Ask a question + - name: Discussion + url: https://github.com/SharpAdb/AdvancedSharpAdbClient/discussions/new?category=general + about: Start a discussion diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml deleted file mode 100644 index cf371cf2..00000000 --- a/.github/ISSUE_TEMPLATE/question.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: Question -description: Ask a question -title: "Question title" -labels: [question] -body: - - type: textarea - validations: - required: true - attributes: - label: Describe your question - description: A clear and concise description of your question. - placeholder: Can I ask that [...] \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index deb58046..09af876b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,6 +8,9 @@ on: pull_request: branches: - main + paths: + - '.github/workflows/build-and-test.yml' + - 'AdvancedSharpAdbClient**' workflow_dispatch: env: @@ -23,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -31,7 +34,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} - dotnet-quality: 'preview' - name: Install Dependencies run: dotnet restore -p:FullTargets=false @@ -40,16 +42,16 @@ jobs: run: dotnet build --no-restore -p:FullTargets=false - name: Test - run: dotnet test --no-restore -p:FullTargets=false + run: dotnet test --no-restore --blame-hang-timeout 1m -p:FullTargets=false pack-and-publish: name: pack-and-publish needs: build-and-test - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -57,7 +59,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} - dotnet-quality: 'preview' - name: Install Dependencies run: dotnet restore -p:FullTargets=true @@ -67,7 +68,9 @@ jobs: - name: Publish if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} - run: dotnet nuget push ./nugets/*nupkg --source https://nuget.pkg.github.com/SharpAdb/index.json --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} + run: dotnet nuget push nugets/**.nupkg --source $env:NUGET_SOURCE --skip-duplicate --api-key ${{ secrets.NUGET_KEY }} + env: + NUGET_SOURCE: https://nuget.pkg.github.com/SharpAdb/index.json - name: Upload uses: actions/upload-artifact@v3 diff --git a/AdvancedSharpAdbClient.Tests/AdbClientTests.Async.cs b/AdvancedSharpAdbClient.Tests/AdbClientTests.Async.cs index 88836b50..ef87d743 100644 --- a/AdvancedSharpAdbClient.Tests/AdbClientTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/AdbClientTests.Async.cs @@ -1,20 +1,16 @@ -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; -using System; +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Drawing.Imaging; using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using Xunit; -using System.Data.Common; namespace AdvancedSharpAdbClient.Tests { @@ -26,23 +22,14 @@ public partial class AdbClientTests [Fact] public async void GetAdbVersionAsyncTest() { - string[] responseMessages = new string[] - { - "0020" - }; + string[] responseMessages = ["0020"]; + string[] requests = ["host:version"]; - string[] requests = new string[] - { - "host:version" - }; - - int version = 0; - - await RunTestAsync( + int version = await RunTestAsync( OkResponse, responseMessages, requests, - async () => version = await TestClient.GetAdbVersionAsync()); + () => TestClient.GetAdbVersionAsync()); // Make sure and the correct value is returned. Assert.Equal(32, version); @@ -54,10 +41,7 @@ await RunTestAsync( [Fact] public async void KillAdbAsyncTest() { - string[] requests = new string[] - { - "host:kill" - }; + string[] requests = ["host:kill"]; await RunTestAsync( NoResponses, @@ -72,29 +56,20 @@ await RunTestAsync( [Fact] public async void GetDevicesAsyncTest() { - string[] responseMessages = new string[] - { - "169.254.109.177:5555 device product:VS Emulator 5\" KitKat (4.4) XXHDPI Phone model:5__KitKat__4_4__XXHDPI_Phone device:donatello\n" - }; - - string[] requests = new string[] - { - "host:devices-l" - }; - - IEnumerable devices = null; + string[] responseMessages = ["169.254.109.177:5555 device product:VS Emulator 5\" KitKat (4.4) XXHDPI Phone model:5__KitKat__4_4__XXHDPI_Phone device:donatello\n"]; + string[] requests = ["host:devices-l"]; - await RunTestAsync( + DeviceData[] devices = await RunTestAsync( OkResponse, responseMessages, requests, - async () => devices = await TestClient.GetDevicesAsync()); + async () => await TestClient.GetDevicesAsync().ToArrayAsync()); // Make sure and the correct value is returned. Assert.NotNull(devices); Assert.Single(devices); - DeviceData device = devices.Single(); + DeviceData device = devices.SingleOrDefault(); Assert.Equal("169.254.109.177:5555", device.Serial); Assert.Equal(DeviceState.Online, device.State); @@ -144,15 +119,8 @@ await RunCreateForwardAsyncTest( [Fact] public async void CreateDuplicateForwardAsyncTest() { - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.FromError("cannot rebind existing socket") - }; - - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:forward:norebind:tcp:1;tcp:2" - }; + AdbResponse[] responses = [AdbResponse.FromError("cannot rebind existing socket")]; + string[] requests = ["host-serial:169.254.109.177:5555:forward:norebind:tcp:1;tcp:2"]; _ = await Assert.ThrowsAsync(() => RunTestAsync( @@ -168,10 +136,7 @@ public async void CreateDuplicateForwardAsyncTest() [Fact] public async void RemoveForwardAsyncTest() { - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:killforward:tcp:1" - }; + string[] requests = ["host-serial:169.254.109.177:5555:killforward:tcp:1"]; await RunTestAsync( OkResponse, @@ -186,20 +151,14 @@ await RunTestAsync( [Fact] public async void RemoveReverseForwardAsyncTest() { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reverse:killforward:localabstract:test" - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }; + ]; await RunTestAsync( - responses, + OkResponses(2), NoResponseMessages, requests, () => TestClient.RemoveReverseForwardAsync(Device, "localabstract:test")); @@ -211,10 +170,7 @@ await RunTestAsync( [Fact] public async void RemoveAllForwardsAsyncTest() { - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:killforward-all" - }; + string[] requests = ["host-serial:169.254.109.177:5555:killforward-all"]; await RunTestAsync( OkResponse, @@ -229,20 +185,14 @@ await RunTestAsync( [Fact] public async void RemoveAllReversesAsyncTest() { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reverse:killforward-all" - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }; + ]; await RunTestAsync( - responses, + OkResponses(2), NoResponseMessages, requests, () => TestClient.RemoveAllReverseForwardsAsync(Device)); @@ -254,23 +204,14 @@ await RunTestAsync( [Fact] public async void ListForwardAsyncTest() { - string[] responseMessages = new string[] - { - "169.254.109.177:5555 tcp:1 tcp:2\n169.254.109.177:5555 tcp:3 tcp:4\n169.254.109.177:5555 tcp:5 local:/socket/1\n" - }; - - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:list-forward" - }; - - ForwardData[] forwards = null; + string[] responseMessages = ["169.254.109.177:5555 tcp:1 tcp:2\n169.254.109.177:5555 tcp:3 tcp:4\n169.254.109.177:5555 tcp:5 local:/socket/1\n"]; + string[] requests = ["host-serial:169.254.109.177:5555:list-forward"]; - await RunTestAsync( + ForwardData[] forwards = await RunTestAsync( OkResponse, responseMessages, requests, - async () => forwards = (await TestClient.ListForwardAsync(Device)).ToArray()); + async () => await TestClient.ListForwardAsync(Device).ToArrayAsync()); Assert.NotNull(forwards); Assert.Equal(3, forwards.Length); @@ -285,29 +226,19 @@ await RunTestAsync( [Fact] public async void ListReverseForwardAsyncTest() { - string[] responseMessages = new string[] - { - "(reverse) localabstract:scrcpy tcp:100\n(reverse) localabstract: scrcpy2 tcp:100\n(reverse) localabstract: scrcpy3 tcp:100\n" - }; - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }; + string[] responseMessages = ["(reverse) localabstract:scrcpy tcp:100\n(reverse) localabstract: scrcpy2 tcp:100\n(reverse) localabstract: scrcpy3 tcp:100\n"]; - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reverse:list-forward" - }; - - ForwardData[] forwards = null; + ]; - await RunTestAsync( - responses, + ForwardData[] forwards = await RunTestAsync( + OkResponses(2), responseMessages, requests, - async () => forwards = (await TestClient.ListReverseForwardAsync(Device)).ToArray()); + async () => await TestClient.ListReverseForwardAsync(Device).ToArrayAsync()); Assert.NotNull(forwards); Assert.Equal(3, forwards.Length); @@ -317,30 +248,41 @@ await RunTestAsync( } /// - /// Tests the method. + /// Tests the method. /// [Fact] - public async void ExecuteRemoteCommandAsyncTest() + public async void ExecuteServerCommandAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; + string[] requests = ["host:version"]; - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }; + byte[] streamData = Encoding.ASCII.GetBytes("0020"); + await using MemoryStream shellStream = new(streamData); - string[] responseMessages = Array.Empty(); + ConsoleOutputReceiver receiver = new(); - string[] requests = new string[] - { + await RunTestAsync( + OkResponse, + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ExecuteServerCommandAsync("host", "version", receiver)); + + string version = receiver.ToString(); + Assert.Equal("0020\r\n", receiver.ToString(), ignoreLineEndingDifferences: true); + Assert.Equal(32, int.Parse(version, NumberStyles.HexNumber)); + } + + /// + /// Tests the method. + /// + [Fact] + public async void ExecuteRemoteCommandAsyncTest() + { + string[] requests = + [ "host:transport:169.254.109.177:5555", "shell:echo Hello, World" - }; + ]; byte[] streamData = Encoding.ASCII.GetBytes("Hello, World\r\n"); await using MemoryStream shellStream = new(streamData); @@ -348,50 +290,36 @@ public async void ExecuteRemoteCommandAsyncTest() ConsoleOutputReceiver receiver = new(); await RunTestAsync( - responses, - responseMessages, + OkResponses(2), + NoResponseMessages, requests, - shellStream, - () => TestClient.ExecuteRemoteCommandAsync("echo Hello, World", device, receiver)); + [shellStream], + () => TestClient.ExecuteRemoteCommandAsync("echo Hello, World", Device, receiver)); Assert.Equal("Hello, World\r\n", receiver.ToString(), ignoreLineEndingDifferences: true); } /// - /// Tests the method. + /// Tests the method. /// [Fact] public async void ExecuteRemoteCommandAsyncUnresponsiveTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }; - - string[] responseMessages = Array.Empty(); - - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "shell:echo Hello, World" - }; + ]; ConsoleOutputReceiver receiver = new(); _ = await Assert.ThrowsAsync(() => RunTestAsync( - responses, - responseMessages, + OkResponses(2), + NoResponseMessages, requests, null, - () => TestClient.ExecuteRemoteCommandAsync("echo Hello, World", device, receiver, CancellationToken.None))); + () => TestClient.ExecuteRemoteCommandAsync("echo Hello, World", Device, receiver))); } /// @@ -400,30 +328,27 @@ public async void ExecuteRemoteCommandAsyncUnresponsiveTest() [Fact] public async void GetFrameBufferAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - DummyAdbSocket socket = new(); - - socket.Responses.Enqueue(AdbResponse.OK); - socket.Responses.Enqueue(AdbResponse.OK); - - socket.Requests.Add("host:transport:169.254.109.177:5555"); - socket.Requests.Add("framebuffer:"); - - socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebufferheader.bin")); - socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebuffer.bin")); + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "framebuffer:" + ]; - Framebuffer framebuffer = null; + Framebuffer framebuffer = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + NoSyncRequests, + NoSyncResponses, + [ + await File.ReadAllBytesAsync("Assets/framebufferheader.bin"), + await File.ReadAllBytesAsync("Assets/framebuffer.bin") + ], + null, + () => TestClient.GetFrameBufferAsync(Device)); - Factories.AdbSocketFactory = (endPoint) => socket; - framebuffer = await TestClient.GetFrameBufferAsync(device); - Assert.NotNull(framebuffer); - Assert.Equal(device, framebuffer.Device); + Assert.Equal(Device, framebuffer.Device); Assert.Equal(16, framebuffer.Data.Length); FramebufferHeader header = framebuffer.Header; @@ -443,64 +368,49 @@ public async void GetFrameBufferAsyncTest() Assert.Equal(1u, header.Version); Assert.Equal(0u, header.ColorSpace); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - using Bitmap image = framebuffer.ToImage(); - Assert.NotNull(image); - Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); - - Assert.Equal(1, image.Width); - Assert.Equal(1, image.Height); - - Color pixel = image.GetPixel(0, 0); - Assert.Equal(0x35, pixel.R); - Assert.Equal(0x4a, pixel.G); - Assert.Equal(0x4c, pixel.B); - Assert.Equal(0xff, pixel.A); - } +#if WINDOWS + using Bitmap image = framebuffer.ToImage(); + Assert.NotNull(image); + Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); + + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + + Color pixel = image.GetPixel(0, 0); + Assert.Equal(0x35, pixel.R); + Assert.Equal(0x4a, pixel.G); + Assert.Equal(0x4c, pixel.B); + Assert.Equal(0xff, pixel.A); +#endif framebuffer.Dispose(); } /// - /// Tests the method. + /// Tests the method. /// [Fact] public async void RunLogServiceAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }; - - string[] responseMessages = Array.Empty(); - - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "shell:logcat -B -b system" - }; + ]; ConsoleOutputReceiver receiver = new(); - await using Stream stream = File.OpenRead("Assets/logcat.bin"); + await using FileStream stream = File.OpenRead("Assets/logcat.bin"); await using ShellStream shellStream = new(stream, false); - Collection logs = new(); + List logs = []; Action sink = logs.Add; await RunTestAsync( - responses, - responseMessages, + OkResponses(2), + NoResponseMessages, requests, - shellStream, - () => TestClient.RunLogServiceAsync(device, sink, CancellationToken.None, LogId.System)); + [shellStream], + () => TestClient.RunLogServiceAsync(Device, sink, LogId.System)); Assert.Equal(3, logs.Count); } @@ -511,14 +421,14 @@ await RunTestAsync( [Fact] public async void RebootAsyncTest() { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reboot:" - }; + ]; await RunTestAsync( - new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, + OkResponses(2), NoResponseMessages, requests, () => TestClient.RebootAsync(Device)); @@ -662,8 +572,8 @@ public async void ConnectAsyncHostEndpointNullTest() => [Fact] public async void DisconnectAsyncTest() { - string[] requests = new string[] { "host:disconnect:localhost:5555" }; - string[] responseMessages = new string[] { "disconnected 127.0.0.1:5555" }; + string[] requests = ["host:disconnect:localhost:5555"]; + string[] responseMessages = ["disconnected 127.0.0.1:5555"]; await RunTestAsync( OkResponse, @@ -678,32 +588,25 @@ await RunTestAsync( [Fact] public async void RootAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "root:" - }; + ]; byte[] expectedData = new byte[1024]; - byte[] expectedString = Encoding.UTF8.GetBytes("adbd cannot run as root in production builds\n"); - Buffer.BlockCopy(expectedString, 0, expectedData, 0, expectedString.Length); + "adbd cannot run as root in production builds\n"u8.CopyTo(expectedData); _ = await Assert.ThrowsAsync(() => RunTestAsync( - new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { expectedData }, - Array.Empty(), - () => TestClient.RootAsync(device))); + NoSyncRequests, + NoSyncResponses, + [expectedData], + null, + () => TestClient.RootAsync(Device))); } /// @@ -712,56 +615,43 @@ public async void RootAsyncTest() [Fact] public async void UnrootAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "unroot:" - }; + ]; byte[] expectedData = new byte[1024]; - byte[] expectedString = Encoding.UTF8.GetBytes("adbd not running as root\n"); - Buffer.BlockCopy(expectedString, 0, expectedData, 0, expectedString.Length); + "adbd not running as root\n"u8.CopyTo(expectedData); _ = await Assert.ThrowsAsync(() => RunTestAsync( - new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { expectedData }, - Array.Empty(), - () => TestClient.UnrootAsync(device))); + NoSyncRequests, + NoSyncResponses, + [expectedData], + null, + () => TestClient.UnrootAsync(Device))); } /// - /// Tests the method. + /// Tests the method. /// [Fact] public async void InstallAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install' -S 205774" - }; + ]; // The app data is sent in chunks of 32 kb - Collection applicationDataChuncks = new(); + List applicationDataChunks = []; - await using (Stream stream = File.OpenRead("Assets/testapp.apk")) + await using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { while (true) { @@ -775,64 +665,48 @@ public async void InstallAsyncTest() else { buffer = buffer.Take(read).ToArray(); - applicationDataChuncks.Add(buffer); + applicationDataChunks.Add(buffer); } } } - byte[] response = Encoding.UTF8.GetBytes("Success\n"); + byte[] response = "Success\n"u8.ToArray(); - await using (Stream stream = File.OpenRead("Assets/testapp.apk")) + await using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { response }, - applicationDataChuncks.ToArray(), - () => TestClient.InstallAsync(device, stream)); + NoSyncRequests, + NoSyncResponses, + [response], + applicationDataChunks.ToArray(), + () => TestClient.InstallAsync(Device, stream)); } } /// - /// Tests the method. + /// Tests the method. /// [Fact] public async void InstallCreateAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install-create' -p com.google.android.gms" - }; + ]; byte[] streamData = Encoding.ASCII.GetBytes("Success: created install session [936013062]\r\n"); await using MemoryStream shellStream = new(streamData); - string session = string.Empty; - - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + string session = await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - shellStream, - async () => session = await TestClient.InstallCreateAsync(device, "com.google.android.gms")); + [shellStream], + () => TestClient.InstallCreateAsync(Device, "com.google.android.gms")); Assert.Equal("936013062", session); } @@ -843,22 +717,16 @@ await RunTestAsync( [Fact] public async void InstallWriteAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install-write' -S 205774 936013062 base.apk" - }; + ]; // The app data is sent in chunks of 32 kb - Collection applicationDataChuncks = new(); + List applicationDataChunks = []; - await using (Stream stream = File.OpenRead("Assets/testapp.apk")) + await using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { while (true) { @@ -872,28 +740,24 @@ public async void InstallWriteAsyncTest() else { buffer = buffer.Take(read).ToArray(); - applicationDataChuncks.Add(buffer); + applicationDataChunks.Add(buffer); } } } - byte[] response = Encoding.UTF8.GetBytes("Success: streamed 205774 bytes\n"); + byte[] response = "Success: streamed 205774 bytes\n"u8.ToArray(); - await using (Stream stream = File.OpenRead("Assets/testapp.apk")) + await using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { response }, - applicationDataChuncks.ToArray(), - () => TestClient.InstallWriteAsync(device, stream, "base", "936013062")); + NoSyncRequests, + NoSyncResponses, + [response], + applicationDataChunks.ToArray(), + () => TestClient.InstallWriteAsync(Device, stream, "base", "936013062")); } } @@ -903,69 +767,64 @@ await RunTestAsync( [Fact] public async void InstallCommitAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install-commit' 936013062" - }; + ]; byte[] streamData = Encoding.ASCII.GetBytes("Success\r\n"); await using MemoryStream shellStream = new(streamData); await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => TestClient.InstallCommitAsync(device, "936013062")); + [shellStream], + () => TestClient.InstallCommitAsync(Device, "936013062")); } /// - /// Tests the method. + /// Tests the method. /// [Fact] - public async void GetFeatureSetAsyncTest() + public async void UninstallAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "exec:cmd package 'uninstall' com.android.gallery3d" + ]; - string[] requests = new string[] - { - "host-serial:009d1cd696d5194a:features" - }; + byte[] streamData = Encoding.ASCII.GetBytes("Success\r\n"); + using MemoryStream shellStream = new(streamData); - string[] responses = new string[] - { - "sendrecv_v2_brotli,remount_shell,sendrecv_v2,abb_exec,fixed_push_mkdir,fixed_push_symlink_timestamp,abb,shell_v2,cmd,ls_v2,apex,stat_v2\r\n" - }; + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.UninstallAsync(Device, "com.android.gallery3d")); + } - IEnumerable features = null; + /// + /// Tests the method. + /// + [Fact] + public async void GetFeatureSetAsyncTest() + { + string[] requests = ["host-serial:169.254.109.177:5555:features"]; + string[] responses = ["sendrecv_v2_brotli,remount_shell,sendrecv_v2,abb_exec,fixed_push_mkdir,fixed_push_symlink_timestamp,abb,shell_v2,cmd,ls_v2,apex,stat_v2\r\n"]; - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - }, + string[] features = await RunTestAsync( + OkResponse, responses, requests, - async () => features = await TestClient.GetFeatureSetAsync(device)); + async () => await TestClient.GetFeatureSetAsync(Device).ToArrayAsync()); - Assert.Equal(12, features.Count()); - Assert.Equal("sendrecv_v2_brotli", features.First()); - Assert.Equal("stat_v2", features.Last()); + Assert.Equal(12, features.Length); + Assert.Equal("sendrecv_v2_brotli", features.FirstOrDefault()); + Assert.Equal("stat_v2", features.LastOrDefault()); } /// @@ -974,35 +833,23 @@ await RunTestAsync( [Fact] public async void DumpScreenStringAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); string cleanDump = File.ReadAllText(@"Assets/dumpscreen_clean.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); await using MemoryStream shellStream = new(streamData); - string xml = string.Empty; - - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + string xml = await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - shellStream, - async () => xml = await TestClient.DumpScreenStringAsync(device)); + [shellStream], + () => TestClient.DumpScreenStringAsync(Device)); Assert.Equal(cleanDump, xml); } @@ -1013,35 +860,23 @@ await RunTestAsync( [Fact] public async void DumpScreenStringAsyncMIUITest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string miuiDump = File.ReadAllText(@"Assets/dumpscreen_miui.txt"); string cleanMIUIDump = File.ReadAllText(@"Assets/dumpscreen_miui_clean.txt"); byte[] miuiStreamData = Encoding.UTF8.GetBytes(miuiDump); await using MemoryStream miuiStream = new(miuiStreamData); - string miuiXml = string.Empty; - - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + string miuiXml = await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - miuiStream, - async () => miuiXml = await TestClient.DumpScreenStringAsync(device)); + [miuiStream], + () => TestClient.DumpScreenStringAsync(Device)); Assert.Equal(cleanMIUIDump, miuiXml); } @@ -1052,32 +887,20 @@ await RunTestAsync( [Fact] public async void DumpScreenStringAsyncEmptyTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; - byte[] emptyStreamData = Encoding.UTF8.GetBytes(string.Empty); - await using MemoryStream emptyStream = new(emptyStreamData); - string emptyXml = string.Empty; + await using MemoryStream emptyStream = new(); - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, - NoResponseMessages, - requests, - emptyStream, - async () => emptyXml = await TestClient.DumpScreenStringAsync(device)); + string emptyXml = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [emptyStream], + () => TestClient.DumpScreenStringAsync(Device)); Assert.True(string.IsNullOrEmpty(emptyXml)); } @@ -1088,17 +911,11 @@ await RunTestAsync( [Fact] public async void DumpScreenStringAsyncErrorTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string errorXml = File.ReadAllText(@"Assets/dumpscreen_error.txt"); byte[] errorStreamData = Encoding.UTF8.GetBytes(errorXml); @@ -1106,15 +923,11 @@ public async void DumpScreenStringAsyncErrorTest() await Assert.ThrowsAsync(() => RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - errorStream, - () => TestClient.DumpScreenStringAsync(device))); + [errorStream], + () => TestClient.DumpScreenStringAsync(Device))); } /// @@ -1123,34 +936,22 @@ await Assert.ThrowsAsync(() => [Fact] public async void DumpScreenAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); await using MemoryStream shellStream = new(streamData); - XmlDocument xml = null; - - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + XmlDocument xml = await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - shellStream, - async () => xml = await TestClient.DumpScreenAsync(device)); + [shellStream], + () => TestClient.DumpScreenAsync(Device)); string cleanDump = File.ReadAllText(@"Assets/dumpscreen_clean.txt"); XmlDocument doc = new(); @@ -1159,25 +960,52 @@ await RunTestAsync( Assert.Equal(doc, xml); } +#if WINDOWS10_0_17763_0_OR_GREATER + /// + /// Tests the method. + /// + [Fact] + public async void DumpScreenWinRTAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:uiautomator dump /dev/tty" + ]; + + string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); + byte[] streamData = Encoding.UTF8.GetBytes(dump); + await using MemoryStream shellStream = new(streamData); + + Windows.Data.Xml.Dom.XmlDocument xml = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.DumpScreenWinRTAsync(Device)); + + string cleanDump = File.ReadAllText(@"Assets/dumpscreen_clean.txt"); + Windows.Data.Xml.Dom.XmlDocument doc = new(); + doc.LoadXml(cleanDump); + + Assert.Equal(doc.InnerText, xml.InnerText); + } + +#endif + /// /// Tests the method. /// [Fact] public async void ClickAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:input tap 100 100" - }; + ]; - byte[] streamData = Encoding.UTF8.GetBytes(@"java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission + byte[] streamData = @"java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) at android.os.Parcel.createException(Parcel.java:2357) at android.os.Parcel.readException(Parcel.java:2340) @@ -1198,20 +1026,16 @@ at com.android.server.input.InputManagerService.injectInputEventInternal(InputMa at com.android.server.input.InputManagerService.injectInputEvent(InputManagerService.java:651) at android.hardware.input.IInputManager$Stub.onTransact(IInputManager.java:430) at android.os.Binder.execTransactInternal(Binder.java:1165) - at android.os.Binder.execTransact(Binder.java:1134)"); + at android.os.Binder.execTransact(Binder.java:1134)"u8.ToArray(); await using MemoryStream shellStream = new(streamData); JavaException exception = await Assert.ThrowsAsync(() => RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => TestClient.ClickAsync(device, 100, 100))); + [shellStream], + () => TestClient.ClickAsync(Device, 100, 100))); Assert.Equal("SecurityException", exception.JavaName); Assert.Equal("Injecting to another application requires INJECT_EVENTS permission", exception.Message); @@ -1239,37 +1063,191 @@ at android.os.Binder.execTransactInternal(Binder.java:1165) } /// - /// Tests the method. + /// Tests the method. /// [Fact] public async void ClickCordsAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:input tap 100 100" - }; - - byte[] streamData = Encoding.UTF8.GetBytes(@"Error: Injecting to another application requires INJECT_EVENTS permission"); + ]; + + byte[] streamData = "Error: Injecting to another application requires INJECT_EVENTS permission\r\n"u8.ToArray(); await using MemoryStream shellStream = new(streamData); _ = await Assert.ThrowsAsync(() => RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ClickAsync(Device, new Point(100, 100)))); + } + + /// + /// Tests the method. + /// + [Fact] + public async void SwipeAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input swipe 100 200 300 400 500" + ]; + + await using MemoryStream shellStream = new(); + + await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => TestClient.ClickAsync(device, new Cords(100, 100)))); + [shellStream], + () => TestClient.SwipeAsync(Device, 100, 200, 300, 400, 500)); + } + + /// + /// Tests the method. + /// + [Fact] + public async void SwipeAsyncElementTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input swipe 100 200 300 400 500" + ]; + + await using MemoryStream shellStream = new(); + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.SwipeAsync(Device, new Element(TestClient, Device, new Rectangle(0, 0, 200, 400)), new Element(TestClient, Device, new Rectangle(0, 0, 600, 800)), 500)); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("21216 27761\r\n", true)] + [InlineData(" 21216 27761\r\n", true)] + [InlineData(" \r\n", false)] + [InlineData("\r\n", false)] + [InlineData(" ", false)] + [InlineData("", false)] + public async void IsAppRunningAsyncTest(string response, bool expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:pidof com.google.android.gms" + ]; + + byte[] streamData = Encoding.UTF8.GetBytes(response); + await using MemoryStream shellStream = new(streamData); + + bool result = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.IsAppRunningAsync(Device, "com.google.android.gms")); + + Assert.Equal(expected, result); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("app.lawnchair", true)] + [InlineData("com.android.settings", true)] + [InlineData("com.google.android.gms", false)] + public async void IsAppInForegroundAsyncTest(string packageName, bool expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:dumpsys activity activities | grep mResumedActivity" + ]; + + byte[] streamData = @" mResumedActivity: ActivityRecord{1f5309a u0 com.android.settings/.homepage.SettingsHomepageActivity t61029} + mResumedActivity: ActivityRecord{896cc3 u0 app.lawnchair/.LawnchairLauncher t5}"u8.ToArray(); + await using MemoryStream shellStream = new(streamData); + + bool result = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.IsAppInForegroundAsync(Device, packageName)); + + Assert.Equal(expected, result); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("com.google.android.gms", "21216 27761\r\n", AppStatus.Background)] + [InlineData("com.android.gallery3d", "\r\n", AppStatus.Stopped)] + public async void GetAppStatusAsyncTest(string packageName, string response, AppStatus expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:dumpsys activity activities | grep mResumedActivity", + "host:transport:169.254.109.177:5555", + $"shell:pidof {packageName}" + ]; + + byte[] activityData = @" mResumedActivity: ActivityRecord{1f5309a u0 com.android.settings/.homepage.SettingsHomepageActivity t61029} + mResumedActivity: ActivityRecord{896cc3 u0 app.lawnchair/.LawnchairLauncher t5}"u8.ToArray(); + await using MemoryStream activityStream = new(activityData); + byte[] pidData = Encoding.UTF8.GetBytes(response); + await using MemoryStream pidStream = new(pidData); + + AppStatus result = await RunTestAsync( + OkResponses(4), + NoResponseMessages, + requests, + [activityStream, pidStream], + () => TestClient.GetAppStatusAsync(Device, packageName)); + + Assert.Equal(expected, result); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("app.lawnchair", AppStatus.Foreground)] + [InlineData("com.android.settings", AppStatus.Foreground)] + public async void GetAppStatusAsyncForegroundTest(string packageName, AppStatus expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:dumpsys activity activities | grep mResumedActivity" + ]; + + byte[] streamData = @" mResumedActivity: ActivityRecord{1f5309a u0 com.android.settings/.homepage.SettingsHomepageActivity t61029} + mResumedActivity: ActivityRecord{896cc3 u0 app.lawnchair/.LawnchairLauncher t5}"u8.ToArray(); + await using MemoryStream shellStream = new(streamData); + + AppStatus result = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.GetAppStatusAsync(Device, packageName)); + + Assert.Equal(expected, result); } /// @@ -1278,39 +1256,29 @@ public async void ClickCordsAsyncTest() [Fact] public async void FindElementAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); await using MemoryStream shellStream = new(streamData); - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + Element element = await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - shellStream, - async () => - { - Element element = await TestClient.FindElementAsync(device); - Assert.Equal(144, element.GetChildCount()); - element = element[0][0][0][0][0][0][0][0][2][1][0][0]; - Assert.Equal("where-where", element.Attributes["text"]); - Assert.Equal(Area.FromLTRB(45, 889, 427, 973), element.Area); - }); + [shellStream], + () => TestClient.FindElementAsync(Device)); + + Assert.Equal(144, element.GetChildCount()); + Element child = element[0][0][0][0][0][0][0][0][2][1][0][0]; + Assert.Equal("where-where", child.Text); + Assert.Equal(Rectangle.FromLTRB(45, 889, 427, 973), child.Bounds); + Assert.Equal(child, element.FindDescendantOrSelf(x => x.Text == "where-where")); + Assert.Equal(2, element.FindDescendants().Where(x => x.Text == "where-where").Count()); } /// @@ -1319,54 +1287,225 @@ await RunTestAsync( [Fact] public async void FindElementsAsyncTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:uiautomator dump /dev/tty" + ]; - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); + byte[] streamData = Encoding.UTF8.GetBytes(dump); + await using MemoryStream shellStream = new(streamData); + + Element[] elements = await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + async () => await TestClient.FindElementsAsync(Device).ToArrayAsync()); + + int childCount = elements.Length; + Array.ForEach(elements, x => childCount += x.GetChildCount()); + Assert.Equal(145, childCount); + Element element = elements[0][0][0][0][0][0][0][0][0][2][1][0][0]; + Assert.Equal("where-where", element.Attributes["text"]); + Assert.Equal(Rectangle.FromLTRB(45, 889, 427, 973), element.Bounds); + } + + /// + /// Tests the method. + /// + [Fact] + public async void FindAsyncElementsTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); await using MemoryStream shellStream = new(streamData); - await RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + List elements = await RunTestAsync( + OkResponses(2), NoResponseMessages, requests, - shellStream, + [shellStream], async () => { - List elements = await TestClient.FindElementsAsync(device); - int childCount = elements.Count; - elements.ForEach(x => childCount += x.GetChildCount()); - Assert.Equal(145, childCount); - Element element = elements[0][0][0][0][0][0][0][0][0][2][1][0][0]; - Assert.Equal("where-where", element.Attributes["text"]); - Assert.Equal(Area.FromLTRB(45, 889, 427, 973), element.Area); + List elements = []; + await foreach (Element element in TestClient.FindAsyncElements(Device)) + { + elements.Add(element); + } + return elements; }); + + int childCount = elements.Count; + elements.ForEach(x => childCount += x.GetChildCount()); + Assert.Equal(145, childCount); + Element element = elements[0][0][0][0][0][0][0][0][0][2][1][0][0]; + Assert.Equal("where-where", element.Attributes["text"]); + Assert.Equal(Rectangle.FromLTRB(45, 889, 427, 973), element.Bounds); } - private Task RunConnectAsyncTest(Func test, string connectString) + /// + /// Tests the method. + /// + [Fact] + public async void SendKeyEventAsyncTest() { - string[] requests = new string[] - { - $"host:connect:{connectString}" - }; + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_MOVE_END" + ]; - string[] responseMessages = new string[] - { - $"connected to {connectString}" - }; + await using MemoryStream shellStream = new(); + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.SendKeyEventAsync(Device, "KEYCODE_MOVE_END")); + } + + /// + /// Tests the method. + /// + [Fact] + public async void SendTextAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input text Hello, World", + ]; + + await using MemoryStream shellStream = new(); + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.SendTextAsync(Device, "Hello, World")); + } + + /// + /// Tests the method. + /// + [Fact] + public async void ClearInputAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_MOVE_END", + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_DEL KEYCODE_DEL KEYCODE_DEL" + ]; + + await using MemoryStream firstShellStream = new(); + await using MemoryStream secondShellStream = new(); + + await RunTestAsync( + OkResponses(4), + NoResponseMessages, + requests, + [firstShellStream, secondShellStream], + () => TestClient.ClearInputAsync(Device, 3)); + } + + /// + /// Tests the method. + /// + [Fact] + public async void StartAppAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:monkey -p com.android.settings 1", + ]; + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + () => TestClient.StartAppAsync(Device, "com.android.settings")); + } + + /// + /// Tests the method. + /// + [Fact] + public async void StopAppAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:am force-stop com.android.settings", + ]; + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + () => TestClient.StopAppAsync(Device, "com.android.settings")); + } + + /// + /// Tests the method. + /// + [Fact] + public async void ClickBackButtonAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_BACK" + ]; + + await using MemoryStream shellStream = new(); + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ClickBackButtonAsync(Device)); + } + + /// + /// Tests the method. + /// + [Fact] + public async void ClickHomeButtonAsyncTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_HOME" + ]; + + await using MemoryStream shellStream = new(); + + await RunTestAsync( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ClickHomeButtonAsync(Device)); + } + + private Task RunConnectAsyncTest(Func test, string connectString) + { + string[] requests = [$"host:connect:{connectString}"]; + string[] responseMessages = [$"connected to {connectString}"]; return RunTestAsync( OkResponse, @@ -1377,15 +1516,8 @@ private Task RunConnectAsyncTest(Func test, string connectString) private Task RunPairAsyncTest(Func test, string connectString, string code) { - string[] requests = new string[] - { - $"host:pair:{code}:{connectString}" - }; - - string[] responseMessages = new string[] - { - $"Successfully paired to {connectString} [guid=adb-996198a3-xPRwsQ]" - }; + string[] requests = [$"host:pair:{code}:{connectString}"]; + string[] responseMessages = [$"Successfully paired to {connectString} [guid=adb-996198a3-xPRwsQ]"]; return RunTestAsync( OkResponse, @@ -1396,44 +1528,26 @@ private Task RunPairAsyncTest(Func test, string connectString, string code private Task RunCreateReverseAsyncTest(Func test, string reverseString) { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", $"reverse:forward:{reverseString}", - }; + ]; return RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - AdbResponse.OK - }, - new string[] - { - null - }, + OkResponses(3), + [null], requests, () => test(Device)); } private Task RunCreateForwardAsyncTest(Func test, string forwardString) { - string[] requests = new string[] - { - $"host-serial:169.254.109.177:5555:forward:{forwardString}" - }; + string[] requests = [$"host-serial:169.254.109.177:5555:forward:{forwardString}"]; return RunTestAsync( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }, - new string[] - { - null - }, + OkResponses(2), + [null], requests, () => test(Device)); } diff --git a/AdvancedSharpAdbClient.Tests/AdbClientTests.cs b/AdvancedSharpAdbClient.Tests/AdbClientTests.cs index c4a5d832..bff1ee48 100644 --- a/AdvancedSharpAdbClient.Tests/AdbClientTests.cs +++ b/AdvancedSharpAdbClient.Tests/AdbClientTests.cs @@ -1,18 +1,14 @@ -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; -using System; +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Drawing.Imaging; using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Runtime.InteropServices; using System.Text; using System.Xml; using Xunit; -using System.Text.RegularExpressions; namespace AdvancedSharpAdbClient.Tests { @@ -85,23 +81,14 @@ public void CreateAdbForwardRequestTest() [Fact] public void GetAdbVersionTest() { - string[] responseMessages = new string[] - { - "0020" - }; + string[] responseMessages = ["0020"]; + string[] requests = ["host:version"]; - string[] requests = new string[] - { - "host:version" - }; - - int version = 0; - - RunTest( + int version = RunTest( OkResponse, responseMessages, requests, - () => version = TestClient.GetAdbVersion()); + TestClient.GetAdbVersion); // Make sure and the correct value is returned. Assert.Equal(32, version); @@ -113,10 +100,7 @@ public void GetAdbVersionTest() [Fact] public void KillAdbTest() { - string[] requests = new string[] - { - "host:kill" - }; + string[] requests = ["host:kill"]; RunTest( NoResponses, @@ -131,29 +115,20 @@ public void KillAdbTest() [Fact] public void GetDevicesTest() { - string[] responseMessages = new string[] - { - "169.254.109.177:5555 device product:VS Emulator 5\" KitKat (4.4) XXHDPI Phone model:5__KitKat__4_4__XXHDPI_Phone device:donatello\n" - }; - - string[] requests = new string[] - { - "host:devices-l" - }; - - IEnumerable devices = null; + string[] responseMessages = ["169.254.109.177:5555 device product:VS Emulator 5\" KitKat (4.4) XXHDPI Phone model:5__KitKat__4_4__XXHDPI_Phone device:donatello\n"]; + string[] requests = ["host:devices-l"]; - RunTest( + DeviceData[] devices = RunTest( OkResponse, responseMessages, requests, - () => devices = TestClient.GetDevices()); + () => TestClient.GetDevices().ToArray()); // Make sure and the correct value is returned. Assert.NotNull(devices); Assert.Single(devices); - DeviceData device = devices.Single(); + DeviceData device = devices.SingleOrDefault(); Assert.Equal("169.254.109.177:5555", device.Serial); Assert.Equal(DeviceState.Online, device.State); @@ -167,10 +142,7 @@ public void GetDevicesTest() [Fact] public void SetDeviceTest() { - string[] requests = new string[] - { - "host:transport:169.254.109.177:5555" - }; + string[] requests = ["host:transport:169.254.109.177:5555"]; RunTest( OkResponse, @@ -185,14 +157,11 @@ public void SetDeviceTest() [Fact] public void SetInvalidDeviceTest() { - string[] requests = new string[] - { - "host:transport:169.254.109.177:5555" - }; + string[] requests = ["host:transport:169.254.109.177:5555"]; _ = Assert.Throws(() => RunTest( - new AdbResponse[] { AdbResponse.FromError("device not found") }, + [AdbResponse.FromError("device not found")], NoResponseMessages, requests, () => Socket.SetDevice(Device))); @@ -204,14 +173,11 @@ public void SetInvalidDeviceTest() [Fact] public void SetDeviceOtherException() { - string[] requests = new string[] - { - "host:transport:169.254.109.177:5555" - }; + string[] requests = ["host:transport:169.254.109.177:5555"]; _ = Assert.Throws(() => RunTest( - new AdbResponse[] { AdbResponse.FromError("Too many cats.") }, + [AdbResponse.FromError("Too many cats.")], NoResponseMessages, requests, () => Socket.SetDevice(Device))); @@ -259,15 +225,8 @@ public void CreateSocketForwardTest() => [Fact] public void CreateDuplicateForwardTest() { - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.FromError("cannot rebind existing socket") - }; - - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:forward:norebind:tcp:1;tcp:2" - }; + AdbResponse[] responses = [AdbResponse.FromError("cannot rebind existing socket")]; + string[] requests = ["host-serial:169.254.109.177:5555:forward:norebind:tcp:1;tcp:2"]; _ = Assert.Throws(() => RunTest( @@ -283,10 +242,7 @@ public void CreateDuplicateForwardTest() [Fact] public void RemoveForwardTest() { - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:killforward:tcp:1" - }; + string[] requests = ["host-serial:169.254.109.177:5555:killforward:tcp:1"]; RunTest( OkResponse, @@ -301,20 +257,14 @@ public void RemoveForwardTest() [Fact] public void RemoveReverseForwardTest() { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reverse:killforward:localabstract:test" - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }; + ]; RunTest( - responses, + OkResponses(2), NoResponseMessages, requests, () => TestClient.RemoveReverseForward(Device, "localabstract:test")); @@ -326,10 +276,7 @@ public void RemoveReverseForwardTest() [Fact] public void RemoveAllForwardsTest() { - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:killforward-all" - }; + string[] requests = ["host-serial:169.254.109.177:5555:killforward-all"]; RunTest( OkResponse, @@ -344,20 +291,14 @@ public void RemoveAllForwardsTest() [Fact] public void RemoveAllReversesTest() { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reverse:killforward-all" - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }; + ]; RunTest( - responses, + OkResponses(2), NoResponseMessages, requests, () => TestClient.RemoveAllReverseForwards(Device)); @@ -369,23 +310,14 @@ public void RemoveAllReversesTest() [Fact] public void ListForwardTest() { - string[] responseMessages = new string[] - { - "169.254.109.177:5555 tcp:1 tcp:2\n169.254.109.177:5555 tcp:3 tcp:4\n169.254.109.177:5555 tcp:5 local:/socket/1\n" - }; - - string[] requests = new string[] - { - "host-serial:169.254.109.177:5555:list-forward" - }; - - ForwardData[] forwards = null; + string[] responseMessages = ["169.254.109.177:5555 tcp:1 tcp:2\n169.254.109.177:5555 tcp:3 tcp:4\n169.254.109.177:5555 tcp:5 local:/socket/1\n"]; + string[] requests = ["host-serial:169.254.109.177:5555:list-forward"]; - RunTest( + ForwardData[] forwards = RunTest( OkResponse, responseMessages, requests, - () => forwards = TestClient.ListForward(Device).ToArray()); + () => TestClient.ListForward(Device).ToArray()); Assert.NotNull(forwards); Assert.Equal(3, forwards.Length); @@ -400,29 +332,19 @@ public void ListForwardTest() [Fact] public void ListReverseForwardTest() { - string[] responseMessages = new string[] - { - "(reverse) localabstract:scrcpy tcp:100\n(reverse) localabstract: scrcpy2 tcp:100\n(reverse) localabstract: scrcpy3 tcp:100\n" - }; - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }; + string[] responseMessages = ["(reverse) localabstract:scrcpy tcp:100\n(reverse) localabstract: scrcpy2 tcp:100\n(reverse) localabstract: scrcpy3 tcp:100\n"]; - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reverse:list-forward" - }; + ]; - ForwardData[] forwards = null; - - RunTest( - responses, + ForwardData[] forwards = RunTest( + OkResponses(2), responseMessages, requests, - () => forwards = TestClient.ListReverseForward(Device).ToArray()); + () => TestClient.ListReverseForward(Device).ToArray()); Assert.NotNull(forwards); Assert.Equal(3, forwards.Length); @@ -432,30 +354,41 @@ public void ListReverseForwardTest() } /// - /// Tests the method. + /// Tests the method. /// [Fact] - public void ExecuteRemoteCommandTest() + public void ExecuteServerCommandTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; + string[] requests = ["host:version"]; - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }; + byte[] streamData = Encoding.ASCII.GetBytes("0020"); + using MemoryStream shellStream = new(streamData); - string[] responseMessages = Array.Empty(); + ConsoleOutputReceiver receiver = new(); - string[] requests = new string[] - { + RunTest( + OkResponse, + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ExecuteServerCommand("host", "version", receiver)); + + string version = receiver.ToString(); + Assert.Equal("0020\r\n", receiver.ToString(), ignoreLineEndingDifferences: true); + Assert.Equal(32, int.Parse(version, NumberStyles.HexNumber)); + } + + /// + /// Tests the method. + /// + [Fact] + public void ExecuteRemoteCommandTest() + { + string[] requests = + [ "host:transport:169.254.109.177:5555", "shell:echo Hello, World" - }; + ]; byte[] streamData = Encoding.ASCII.GetBytes("Hello, World\r\n"); using MemoryStream shellStream = new(streamData); @@ -463,92 +396,70 @@ public void ExecuteRemoteCommandTest() ConsoleOutputReceiver receiver = new(); RunTest( - responses, - responseMessages, + OkResponses(2), + NoResponseMessages, requests, - shellStream, - () => TestClient.ExecuteRemoteCommand("echo Hello, World", device, receiver)); + [shellStream], + () => TestClient.ExecuteRemoteCommand("echo Hello, World", Device, receiver)); Assert.Equal("Hello, World\r\n", receiver.ToString(), ignoreLineEndingDifferences: true); } /// - /// Tests the method. + /// Tests the method. /// [Fact] public void ExecuteRemoteCommandUnresponsiveTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }; - - string[] responseMessages = Array.Empty(); - - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "shell:echo Hello, World" - }; + ]; ConsoleOutputReceiver receiver = new(); _ = Assert.Throws(() => RunTest( - responses, - responseMessages, + OkResponses(2), + NoResponseMessages, requests, null, - () => TestClient.ExecuteRemoteCommand("echo Hello, World", device, receiver))); + () => TestClient.ExecuteRemoteCommand("echo Hello, World", Device, receiver))); } [Fact] public void CreateRefreshableFramebufferTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - Framebuffer framebuffer = TestClient.CreateRefreshableFramebuffer(device); + Framebuffer framebuffer = TestClient.CreateRefreshableFramebuffer(Device); Assert.NotNull(framebuffer); - Assert.Equal(device, framebuffer.Device); + Assert.Equal(Device, framebuffer.Device); } [Fact] public void GetFrameBufferTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - DummyAdbSocket socket = new(); - - socket.Responses.Enqueue(AdbResponse.OK); - socket.Responses.Enqueue(AdbResponse.OK); - - socket.Requests.Add("host:transport:169.254.109.177:5555"); - socket.Requests.Add("framebuffer:"); - - socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebufferheader.bin")); - socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebuffer.bin")); - - Framebuffer framebuffer = null; + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "framebuffer:" + ]; - Factories.AdbSocketFactory = (endPoint) => socket; - framebuffer = TestClient.GetFrameBuffer(device); + Framebuffer framebuffer = RunTest( + OkResponses(2), + NoResponseMessages, + requests, + NoSyncRequests, + NoSyncResponses, + [ + File.ReadAllBytes("Assets/framebufferheader.bin"), + File.ReadAllBytes("Assets/framebuffer.bin") + ], + null, + () => TestClient.GetFrameBuffer(Device)); Assert.NotNull(framebuffer); - Assert.Equal(device, framebuffer.Device); + Assert.Equal(Device, framebuffer.Device); Assert.Equal(16, framebuffer.Data.Length); FramebufferHeader header = framebuffer.Header; @@ -568,21 +479,20 @@ public void GetFrameBufferTest() Assert.Equal(1u, header.Version); Assert.Equal(0u, header.ColorSpace); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - using Bitmap image = framebuffer.ToImage(); - Assert.NotNull(image); - Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); - - Assert.Equal(1, image.Width); - Assert.Equal(1, image.Height); - - Color pixel = image.GetPixel(0, 0); - Assert.Equal(0x35, pixel.R); - Assert.Equal(0x4a, pixel.G); - Assert.Equal(0x4c, pixel.B); - Assert.Equal(0xff, pixel.A); - } +#if WINDOWS + using Bitmap image = framebuffer.ToImage(); + Assert.NotNull(image); + Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); + + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + + Color pixel = image.GetPixel(0, 0); + Assert.Equal(0x35, pixel.R); + Assert.Equal(0x4a, pixel.G); + Assert.Equal(0x4c, pixel.B); + Assert.Equal(0xff, pixel.A); +#endif framebuffer.Dispose(); } @@ -590,39 +500,25 @@ public void GetFrameBufferTest() [Fact] public void RunLogServiceTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - AdbResponse[] responses = new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }; - - string[] responseMessages = Array.Empty(); - - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "shell:logcat -B -b system" - }; + ]; ConsoleOutputReceiver receiver = new(); - using Stream stream = File.OpenRead("Assets/logcat.bin"); + using FileStream stream = File.OpenRead("Assets/logcat.bin"); using ShellStream shellStream = new(stream, false); - Collection logs = new(); + List logs = []; Action sink = logs.Add; RunTest( - responses, - responseMessages, + OkResponses(2), + NoResponseMessages, requests, - shellStream, - () => TestClient.RunLogService(device, sink, LogId.System)); + [shellStream], + () => TestClient.RunLogService(Device, sink, LogId.System)); Assert.Equal(3, logs.Count); } @@ -633,14 +529,14 @@ public void RunLogServiceTest() [Fact] public void RebootTest() { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", "reboot:" - }; + ]; RunTest( - new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, + OkResponses(2), NoResponseMessages, requests, () => TestClient.Reboot(Device)); @@ -784,8 +680,8 @@ public void ConnectHostEndpointNullTest() => [Fact] public void DisconnectTest() { - string[] requests = new string[] { "host:disconnect:localhost:5555" }; - string[] responseMessages = new string[] { "disconnected 127.0.0.1:5555" }; + string[] requests = ["host:disconnect:localhost:5555"]; + string[] responseMessages = ["disconnected 127.0.0.1:5555"]; RunTest( OkResponse, @@ -800,32 +696,25 @@ public void DisconnectTest() [Fact] public void RootTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "root:" - }; + ]; byte[] expectedData = new byte[1024]; - byte[] expectedString = Encoding.UTF8.GetBytes("adbd cannot run as root in production builds\n"); - Buffer.BlockCopy(expectedString, 0, expectedData, 0, expectedString.Length); + "adbd cannot run as root in production builds\n"u8.CopyTo(expectedData); _ = Assert.Throws(() => RunTest( - new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { expectedData }, - Array.Empty(), - () => TestClient.Root(device))); + NoSyncRequests, + NoSyncResponses, + [expectedData], + null, + () => TestClient.Root(Device))); } /// @@ -834,32 +723,25 @@ public void RootTest() [Fact] public void UnrootTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "unroot:" - }; + ]; byte[] expectedData = new byte[1024]; - byte[] expectedString = Encoding.UTF8.GetBytes("adbd not running as root\n"); - Buffer.BlockCopy(expectedString, 0, expectedData, 0, expectedString.Length); + "adbd not running as root\n"u8.CopyTo(expectedData); _ = Assert.Throws(() => RunTest( - new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { expectedData }, - Array.Empty(), - () => TestClient.Unroot(device))); + NoSyncRequests, + NoSyncResponses, + [expectedData], + null, + () => TestClient.Unroot(Device))); } /// @@ -868,27 +750,21 @@ public void UnrootTest() [Fact] public void InstallTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install' -S 205774" - }; + ]; // The app data is sent in chunks of 32 kb - Collection applicationDataChuncks = new(); + List applicationDataChunks = []; - using (Stream stream = File.OpenRead("Assets/testapp.apk")) + using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { while (true) { byte[] buffer = new byte[32 * 1024]; - int read = stream.Read(buffer, 0, buffer.Length); + int read = stream.Read(buffer.AsSpan(0, buffer.Length)); if (read == 0) { @@ -897,28 +773,24 @@ public void InstallTest() else { buffer = buffer.Take(read).ToArray(); - applicationDataChuncks.Add(buffer); + applicationDataChunks.Add(buffer); } } } - byte[] response = Encoding.UTF8.GetBytes("Success\n"); + byte[] response = "Success\n"u8.ToArray(); - using (Stream stream = File.OpenRead("Assets/testapp.apk")) + using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { response }, - applicationDataChuncks.ToArray(), - () => TestClient.Install(device, stream)); + NoSyncRequests, + NoSyncResponses, + [response], + applicationDataChunks.ToArray(), + () => TestClient.Install(Device, stream)); } } @@ -928,33 +800,21 @@ public void InstallTest() [Fact] public void InstallCreateTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install-create' -p com.google.android.gms" - }; + ]; byte[] streamData = Encoding.ASCII.GetBytes("Success: created install session [936013062]\r\n"); using MemoryStream shellStream = new(streamData); - string session = string.Empty; - - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + string session = RunTest( + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => session = TestClient.InstallCreate(device, "com.google.android.gms")); + [shellStream], + () => TestClient.InstallCreate(Device, "com.google.android.gms")); Assert.Equal("936013062", session); } @@ -965,27 +825,21 @@ public void InstallCreateTest() [Fact] public void InstallWriteTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install-write' -S 205774 936013062 base.apk" - }; + ]; // The app data is sent in chunks of 32 kb - Collection applicationDataChuncks = new(); + List applicationDataChunks = []; - using (Stream stream = File.OpenRead("Assets/testapp.apk")) + using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { while (true) { byte[] buffer = new byte[32 * 1024]; - int read = stream.Read(buffer, 0, buffer.Length); + int read = stream.Read(buffer.AsSpan(0, buffer.Length)); if (read == 0) { @@ -994,28 +848,24 @@ public void InstallWriteTest() else { buffer = buffer.Take(read).ToArray(); - applicationDataChuncks.Add(buffer); + applicationDataChunks.Add(buffer); } } } - byte[] response = Encoding.UTF8.GetBytes("Success: streamed 205774 bytes\n"); + byte[] response = "Success: streamed 205774 bytes\n"u8.ToArray(); - using (Stream stream = File.OpenRead("Assets/testapp.apk")) + using (FileStream stream = File.OpenRead("Assets/testapp.apk")) { RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - Array.Empty<(SyncCommand, string)>(), - Array.Empty(), - new byte[][] { response }, - applicationDataChuncks.ToArray(), - () => TestClient.InstallWrite(device, stream, "base", "936013062")); + NoSyncRequests, + NoSyncResponses, + [response], + applicationDataChunks.ToArray(), + () => TestClient.InstallWrite(Device, stream, "base", "936013062")); } } @@ -1025,69 +875,64 @@ public void InstallWriteTest() [Fact] public void InstallCommitTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "exec:cmd package 'install-commit' 936013062" - }; + ]; byte[] streamData = Encoding.ASCII.GetBytes("Success\r\n"); using MemoryStream shellStream = new(streamData); RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => TestClient.InstallCommit(device, "936013062")); + [shellStream], + () => TestClient.InstallCommit(Device, "936013062")); } /// - /// Tests the method. + /// Tests the method. /// [Fact] - public void GetFeatureSetTest() + public void UninstallTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "exec:cmd package 'uninstall' com.android.gallery3d" + ]; - string[] requests = new string[] - { - "host-serial:009d1cd696d5194a:features" - }; + byte[] streamData = Encoding.ASCII.GetBytes("Success\r\n"); + using MemoryStream shellStream = new(streamData); - string[] responses = new string[] - { - "sendrecv_v2_brotli,remount_shell,sendrecv_v2,abb_exec,fixed_push_mkdir,fixed_push_symlink_timestamp,abb,shell_v2,cmd,ls_v2,apex,stat_v2\r\n" - }; + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.Uninstall(Device, "com.android.gallery3d")); + } - IEnumerable features = null; + /// + /// Tests the method. + /// + [Fact] + public void GetFeatureSetTest() + { + string[] requests = ["host-serial:169.254.109.177:5555:features"]; + string[] responses = ["sendrecv_v2_brotli,remount_shell,sendrecv_v2,abb_exec,fixed_push_mkdir,fixed_push_symlink_timestamp,abb,shell_v2,cmd,ls_v2,apex,stat_v2\r\n"]; - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - }, + string[] features = RunTest( + OkResponse, responses, requests, - () => features = TestClient.GetFeatureSet(device)); + () => TestClient.GetFeatureSet(Device).ToArray()); - Assert.Equal(12, features.Count()); - Assert.Equal("sendrecv_v2_brotli", features.First()); - Assert.Equal("stat_v2", features.Last()); + Assert.Equal(12, features.Length); + Assert.Equal("sendrecv_v2_brotli", features.FirstOrDefault()); + Assert.Equal("stat_v2", features.LastOrDefault()); } /// @@ -1096,35 +941,23 @@ public void GetFeatureSetTest() [Fact] public void DumpScreenStringTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); string cleanDump = File.ReadAllText(@"Assets/dumpscreen_clean.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); using MemoryStream shellStream = new(streamData); - string xml = string.Empty; - - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + string xml = RunTest( + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => xml = TestClient.DumpScreenString(device)); + [shellStream], + () => TestClient.DumpScreenString(Device)); Assert.Equal(cleanDump, xml); } @@ -1135,35 +968,23 @@ public void DumpScreenStringTest() [Fact] public void DumpScreenStringMIUITest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string miuidump = File.ReadAllText(@"Assets/dumpscreen_miui.txt"); string cleanMIUIDump = File.ReadAllText(@"Assets/dumpscreen_miui_clean.txt"); byte[] miuiStreamData = Encoding.UTF8.GetBytes(miuidump); using MemoryStream miuiStream = new(miuiStreamData); - string miuiXml = string.Empty; - - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + string miuiXml = RunTest( + OkResponses(2), NoResponseMessages, requests, - miuiStream, - () => miuiXml = TestClient.DumpScreenString(device)); + [miuiStream], + () => TestClient.DumpScreenString(Device)); Assert.Equal(cleanMIUIDump, miuiXml); } @@ -1174,32 +995,21 @@ public void DumpScreenStringMIUITest() [Fact] public void DumpScreenStringEmptyTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; byte[] emptyStreamData = Encoding.UTF8.GetBytes(string.Empty); using MemoryStream emptyStream = new(emptyStreamData); - string emptyXml = string.Empty; - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, - NoResponseMessages, - requests, - emptyStream, - () => emptyXml = TestClient.DumpScreenString(device)); + string emptyXml = RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [emptyStream], + () => TestClient.DumpScreenString(Device)); Assert.True(string.IsNullOrEmpty(emptyXml)); } @@ -1210,17 +1020,11 @@ public void DumpScreenStringEmptyTest() [Fact] public void DumpScreenStringErrorTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string errorXml = File.ReadAllText(@"Assets/dumpscreen_error.txt"); byte[] errorStreamData = Encoding.UTF8.GetBytes(errorXml); @@ -1228,15 +1032,11 @@ public void DumpScreenStringErrorTest() Assert.Throws(() => RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, - NoResponseMessages, - requests, - errorStream, - () => TestClient.DumpScreenString(device))); + OkResponses(2), + NoResponseMessages, + requests, + [errorStream], + () => TestClient.DumpScreenString(Device))); } /// @@ -1245,34 +1045,22 @@ public void DumpScreenStringErrorTest() [Fact] public void DumpScreenTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); using MemoryStream shellStream = new(streamData); - XmlDocument xml = null; - - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + XmlDocument xml = RunTest( + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => xml = TestClient.DumpScreen(device)); + [shellStream], + () => TestClient.DumpScreen(Device)); string cleanDump = File.ReadAllText(@"Assets/dumpscreen_clean.txt"); XmlDocument doc = new(); @@ -1281,25 +1069,52 @@ public void DumpScreenTest() Assert.Equal(doc, xml); } +#if WINDOWS10_0_17763_0_OR_GREATER + /// + /// Tests the method. + /// + [Fact] + public void DumpScreenWinRTTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:uiautomator dump /dev/tty" + ]; + + string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); + byte[] streamData = Encoding.UTF8.GetBytes(dump); + using MemoryStream shellStream = new(streamData); + + Windows.Data.Xml.Dom.XmlDocument xml = RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.DumpScreenWinRT(Device)); + + string cleanDump = File.ReadAllText(@"Assets/dumpscreen_clean.txt"); + Windows.Data.Xml.Dom.XmlDocument doc = new(); + doc.LoadXml(cleanDump); + + Assert.Equal(doc.InnerText, xml.InnerText); + } + +#endif + /// /// Tests the method. /// [Fact] public void ClickTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:input tap 100 100" - }; + ]; - byte[] streamData = Encoding.UTF8.GetBytes(@"java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission + byte[] streamData = @"java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) at android.os.Parcel.createException(Parcel.java:2357) at android.os.Parcel.readException(Parcel.java:2340) @@ -1320,20 +1135,16 @@ at com.android.server.input.InputManagerService.injectInputEventInternal(InputMa at com.android.server.input.InputManagerService.injectInputEvent(InputManagerService.java:651) at android.hardware.input.IInputManager$Stub.onTransact(IInputManager.java:430) at android.os.Binder.execTransactInternal(Binder.java:1165) - at android.os.Binder.execTransact(Binder.java:1134)"); + at android.os.Binder.execTransact(Binder.java:1134)"u8.ToArray(); using MemoryStream shellStream = new(streamData); JavaException exception = Assert.Throws(() => RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => TestClient.Click(device, 100, 100))); + [shellStream], + () => TestClient.Click(Device, 100, 100))); Assert.Equal("SecurityException", exception.JavaName); Assert.Equal("Injecting to another application requires INJECT_EVENTS permission", exception.Message); @@ -1361,37 +1172,192 @@ at android.os.Binder.execTransactInternal(Binder.java:1165) } /// - /// Tests the method. + /// Tests the method. /// [Fact] public void ClickCordsTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:input tap 100 100" - }; + ]; - byte[] streamData = Encoding.UTF8.GetBytes(@"Error: Injecting to another application requires INJECT_EVENTS permission"); + byte[] streamData = "Error: Injecting to another application requires INJECT_EVENTS permission\r\n"u8.ToArray(); using MemoryStream shellStream = new(streamData); _ = Assert.Throws(() => RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.Click(Device, new Point(100, 100)))); + } + + /// + /// Tests the method. + /// + [Fact] + public void SwipeTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input swipe 100 200 300 400 500" + ]; + + using MemoryStream shellStream = new(); + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.Swipe(Device, 100, 200, 300, 400, 500)); + } + + /// + /// Tests the method. + /// + [Fact] + public void SwipeElementTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input swipe 100 200 300 400 500" + ]; + + using MemoryStream shellStream = new(); + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.Swipe(Device, new Element(TestClient, Device, new Rectangle(0, 0, 200, 400)), new Element(TestClient, Device, new Rectangle(0, 0, 600, 800)), 500)); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("21216 27761\r\n", true)] + [InlineData(" 21216 27761\r\n", true)] + [InlineData("12836\r\n", true)] + [InlineData(" \r\n", false)] + [InlineData("\r\n", false)] + [InlineData(" ", false)] + [InlineData("", false)] + public void IsAppRunningTest(string response, bool expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:pidof com.google.android.gms" + ]; + + byte[] streamData = Encoding.UTF8.GetBytes(response); + using MemoryStream shellStream = new(streamData); + + bool result = RunTest( + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => TestClient.Click(device, new Cords(100, 100)))); + [shellStream], + () => TestClient.IsAppRunning(Device, "com.google.android.gms")); + + Assert.Equal(expected, result); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("app.lawnchair", true)] + [InlineData("com.android.settings", true)] + [InlineData("com.google.android.gms", false)] + public void IsAppInForegroundTest(string packageName, bool expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:dumpsys activity activities | grep mResumedActivity" + ]; + + byte[] streamData = @" mResumedActivity: ActivityRecord{1f5309a u0 com.android.settings/.homepage.SettingsHomepageActivity t61029} + mResumedActivity: ActivityRecord{896cc3 u0 app.lawnchair/.LawnchairLauncher t5}"u8.ToArray(); + using MemoryStream shellStream = new(streamData); + + bool result = RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.IsAppInForeground(Device, packageName)); + + Assert.Equal(expected, result); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("com.google.android.gms", "21216 27761\r\n", AppStatus.Background)] + [InlineData("com.android.gallery3d", "\r\n", AppStatus.Stopped)] + public void GetAppStatusTest(string packageName, string response, AppStatus expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:dumpsys activity activities | grep mResumedActivity", + "host:transport:169.254.109.177:5555", + $"shell:pidof {packageName}" + ]; + + byte[] activityData = @" mResumedActivity: ActivityRecord{1f5309a u0 com.android.settings/.homepage.SettingsHomepageActivity t61029} + mResumedActivity: ActivityRecord{896cc3 u0 app.lawnchair/.LawnchairLauncher t5}"u8.ToArray(); + using MemoryStream activityStream = new(activityData); + byte[] pidData = Encoding.UTF8.GetBytes(response); + using MemoryStream pidStream = new(pidData); + + AppStatus result = RunTest( + OkResponses(4), + NoResponseMessages, + requests, + [activityStream, pidStream], + () => TestClient.GetAppStatus(Device, packageName)); + + Assert.Equal(expected, result); + } + + /// + /// Tests the method. + /// + [Theory] + [InlineData("app.lawnchair", AppStatus.Foreground)] + [InlineData("com.android.settings", AppStatus.Foreground)] + public void GetAppStatusForegroundTest(string packageName, AppStatus expected) + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:dumpsys activity activities | grep mResumedActivity" + ]; + + byte[] streamData = @" mResumedActivity: ActivityRecord{1f5309a u0 com.android.settings/.homepage.SettingsHomepageActivity t61029} + mResumedActivity: ActivityRecord{896cc3 u0 app.lawnchair/.LawnchairLauncher t5}"u8.ToArray(); + using MemoryStream shellStream = new(streamData); + + AppStatus result = RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.GetAppStatus(Device, packageName)); + + Assert.Equal(expected, result); } /// @@ -1400,39 +1366,32 @@ public void ClickCordsTest() [Fact] public void FindElementTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); using MemoryStream shellStream = new(streamData); - RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + Element element = RunTest( + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => - { - Element element = TestClient.FindElement(device); - Assert.Equal(144, element.GetChildCount()); - element = element[0][0][0][0][0][0][0][0][2][1][0][0]; - Assert.Equal("where-where", element.Attributes["text"]); - Assert.Equal(Area.FromLTRB(45, 889, 427, 973), element.Area); - }); + [shellStream], + () => TestClient.FindElement(Device)); + + Assert.Equal(144, element.GetChildCount()); + Element child = element[0][0][0][0][0][0][0][0][2][1][0][0]; + Assert.Equal("where-where", child.Text); + Assert.Equal("android.widget.TextView", child.Class); + Assert.Equal("com.bilibili.app.in", child.Package); + Assert.Equal("com.bilibili.app.in:id/header_info_name", child.ResourceID); + Assert.Equal(Rectangle.FromLTRB(45, 889, 427, 973), child.Bounds); + Assert.Equal(child, element.FindDescendantOrSelf(x => x.Text == "where-where")); + Assert.Equal(2, element.FindDescendants().Where(x => x.Text == "where-where").Count()); } /// @@ -1441,54 +1400,186 @@ public void FindElementTest() [Fact] public void FindElementsTest() { - DeviceData device = new() - { - Serial = "009d1cd696d5194a", - State = DeviceState.Online - }; - - string[] requests = new string[] - { - "host:transport:009d1cd696d5194a", + string[] requests = + [ + "host:transport:169.254.109.177:5555", "shell:uiautomator dump /dev/tty" - }; + ]; string dump = File.ReadAllText(@"Assets/dumpscreen.txt"); byte[] streamData = Encoding.UTF8.GetBytes(dump); using MemoryStream shellStream = new(streamData); + Element[] elements = RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.FindElements(Device).ToArray()); + + int childCount = elements.Length; + Array.ForEach(elements, x => childCount += x.GetChildCount()); + Assert.Equal(145, childCount); + Element element = elements[0][0][0][0][0][0][0][0][0][2][1][0][0]; + Assert.Equal("where-where", element.Text); + Assert.Equal(Rectangle.FromLTRB(45, 889, 427, 973), element.Bounds); + } + + /// + /// Tests the method. + /// + [Fact] + public void SendKeyEventTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_MOVE_END" + ]; + + using MemoryStream shellStream = new(); + RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - }, + OkResponses(2), NoResponseMessages, requests, - shellStream, - () => - { - List elements = TestClient.FindElements(device).ToList(); - int childCount = elements.Count; - elements.ForEach(x => childCount += x.GetChildCount()); - Assert.Equal(145, childCount); - Element element = elements[0][0][0][0][0][0][0][0][0][2][1][0][0]; - Assert.Equal("where-where", element.Attributes["text"]); - Assert.Equal(Area.FromLTRB(45, 889, 427, 973), element.Area); - }); + [shellStream], + () => TestClient.SendKeyEvent(Device, "KEYCODE_MOVE_END")); } - private void RunConnectTest(Action test, string connectString) + /// + /// Tests the method. + /// + [Fact] + public void SendTextTest() { - string[] requests = new string[] - { - $"host:connect:{connectString}" - }; + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input text Hello, World", + ]; - string[] responseMessages = new string[] - { - $"connected to {connectString}" - }; + using MemoryStream shellStream = new(); + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.SendText(Device, "Hello, World")); + } + + /// + /// Tests the method. + /// + [Fact] + public void ClearInputTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_MOVE_END", + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_DEL KEYCODE_DEL KEYCODE_DEL" + ]; + + using MemoryStream firstShellStream = new(); + using MemoryStream secondShellStream = new(); + + RunTest( + OkResponses(4), + NoResponseMessages, + requests, + [firstShellStream, secondShellStream], + () => TestClient.ClearInput(Device, 3)); + } + + /// + /// Tests the method. + /// + [Fact] + public void StartAppTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:monkey -p com.android.settings 1", + ]; + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + () => TestClient.StartApp(Device, "com.android.settings")); + } + + /// + /// Tests the method. + /// + [Fact] + public void StopAppTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:am force-stop com.android.settings", + ]; + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + () => TestClient.StopApp(Device, "com.android.settings")); + } + + /// + /// Tests the method. + /// + [Fact] + public void ClickBackButtonTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_BACK" + ]; + + using MemoryStream shellStream = new(); + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ClickBackButton(Device)); + } + + /// + /// Tests the method. + /// + [Fact] + public void ClickHomeButtonTest() + { + string[] requests = + [ + "host:transport:169.254.109.177:5555", + "shell:input keyevent KEYCODE_HOME" + ]; + + using MemoryStream shellStream = new(); + + RunTest( + OkResponses(2), + NoResponseMessages, + requests, + [shellStream], + () => TestClient.ClickHomeButton(Device)); + } + + private void RunConnectTest(Action test, string connectString) + { + string[] requests = [$"host:connect:{connectString}"]; + string[] responseMessages = [$"connected to {connectString}"]; RunTest( OkResponse, @@ -1499,15 +1590,8 @@ private void RunConnectTest(Action test, string connectString) private void RunPairTest(Action test, string connectString, string code) { - string[] requests = new string[] - { - $"host:pair:{code}:{connectString}" - }; - - string[] responseMessages = new string[] - { - $"Successfully paired to {connectString} [guid=adb-996198a3-xPRwsQ]" - }; + string[] requests = [$"host:pair:{code}:{connectString}"]; + string[] responseMessages = [$"Successfully paired to {connectString} [guid=adb-996198a3-xPRwsQ]"]; RunTest( OkResponse, @@ -1518,44 +1602,26 @@ private void RunPairTest(Action test, string connectString, string code) private void RunCreateReverseTest(Action test, string reverseString) { - string[] requests = new string[] - { + string[] requests = + [ "host:transport:169.254.109.177:5555", $"reverse:forward:{reverseString}", - }; + ]; RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK, - AdbResponse.OK - }, - new string[] - { - null - }, + OkResponses(3), + [null], requests, () => test(Device)); } private void RunCreateForwardTest(Action test, string forwardString) { - string[] requests = new string[] - { - $"host-serial:169.254.109.177:5555:forward:{forwardString}" - }; + string[] requests = [$"host-serial:169.254.109.177:5555:forward:{forwardString}"]; RunTest( - new AdbResponse[] - { - AdbResponse.OK, - AdbResponse.OK - }, - new string[] - { - null - }, + OkResponses(2), + [null], requests, () => test(Device)); } diff --git a/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.Async.cs b/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.Async.cs index 6430112b..06629c38 100644 --- a/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.Async.cs @@ -1,11 +1,13 @@ -using AdvancedSharpAdbClient.Exceptions; -using System; +using System; using Xunit; namespace AdvancedSharpAdbClient.Tests { public partial class AdbCommandLineClientTests { + /// + /// Tests the method. + /// [Fact] public async void GetVersionAsyncTest() { @@ -13,10 +15,12 @@ public async void GetVersionAsyncTest() { Version = new Version(1, 0, 32) }; - Assert.Equal(new Version(1, 0, 32), await commandLine.GetVersionAsync()); } + /// + /// Tests the method. + /// [Fact] public async void GetVersionAsyncNullTest() { @@ -27,6 +31,9 @@ public async void GetVersionAsyncNullTest() _ = await Assert.ThrowsAsync(() => commandLine.GetVersionAsync()); } + /// + /// Tests the method. + /// [Fact] public async void GetOutdatedVersionAsyncTest() { @@ -34,10 +41,12 @@ public async void GetOutdatedVersionAsyncTest() { Version = new Version(1, 0, 1) }; - _ = await Assert.ThrowsAsync(() => commandLine.GetVersionAsync()); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncTest() { diff --git a/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.cs b/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.cs index 74d33fcb..00d8358c 100644 --- a/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.cs +++ b/AdvancedSharpAdbClient.Tests/AdbCommandLineClientTests.cs @@ -1,5 +1,4 @@ -using AdvancedSharpAdbClient.Exceptions; -using System; +using System; using Xunit; namespace AdvancedSharpAdbClient.Tests @@ -9,6 +8,9 @@ namespace AdvancedSharpAdbClient.Tests /// public partial class AdbCommandLineClientTests { + /// + /// Tests the method. + /// [Fact] public void GetVersionTest() { @@ -16,10 +18,12 @@ public void GetVersionTest() { Version = new Version(1, 0, 32) }; - Assert.Equal(new Version(1, 0, 32), commandLine.GetVersion()); } + /// + /// Tests the method. + /// [Fact] public void GetVersionNullTest() { @@ -30,6 +34,9 @@ public void GetVersionNullTest() _ = Assert.Throws(commandLine.GetVersion); } + /// + /// Tests the method. + /// [Fact] public void GetOutdatedVersionTest() { @@ -37,10 +44,12 @@ public void GetOutdatedVersionTest() { Version = new Version(1, 0, 1) }; - _ = Assert.Throws(commandLine.GetVersion); } + /// + /// Tests the method. + /// [Fact] public void StartServerTest() { diff --git a/AdvancedSharpAdbClient.Tests/AdbServerTests.Async.cs b/AdvancedSharpAdbClient.Tests/AdbServerTests.Async.cs index a844c0dc..4f253a5c 100644 --- a/AdvancedSharpAdbClient.Tests/AdbServerTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/AdbServerTests.Async.cs @@ -1,5 +1,4 @@ -using AdvancedSharpAdbClient.Exceptions; -using NSubstitute; +using NSubstitute; using NSubstitute.ExceptionExtensions; using System; using System.Net.Sockets; @@ -10,19 +9,25 @@ namespace AdvancedSharpAdbClient.Tests { public partial class AdbServerTests { + /// + /// Tests the method. + /// [Fact] public async void GetStatusAsyncNotRunningTest() { - IAdbClient adbClientMock = Substitute.For(); - adbClientMock.GetAdbVersionAsync(Arg.Any()).Throws(new AggregateException(new SocketException(AdbServer.ConnectionRefused))); + IAdbSocket adbSocketMock = Substitute.For(); + adbSocketMock.SendAdbRequestAsync("host:version", Arg.Any()).Throws(new AggregateException(new SocketException(AdbServer.ConnectionRefused))); - AdbServer adbServer = new(adbClientMock, adbCommandLineClientFactory); + AdbServer adbServer = new((endPoint) => adbSocketMock, adbCommandLineClientFactory); AdbServerStatus status = await adbServer.GetStatusAsync(); Assert.False(status.IsRunning); Assert.Null(status.Version); } + /// + /// Tests the method. + /// [Fact] public async void GetStatusAsyncRunningTest() { @@ -40,28 +45,35 @@ public async void GetStatusAsyncRunningTest() Assert.Equal(new Version(1, 0, 32), status.Version); } + /// + /// Tests the method. + /// [Fact] public async void GetStatusAsyncOtherSocketExceptionTest() { adbSocketFactory = (endPoint) => throw new SocketException(); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); _ = await Assert.ThrowsAsync(async () => await adbServer.GetStatusAsync()); } + /// + /// Tests the method. + /// [Fact] public async void GetStatusAsyncOtherExceptionTest() { adbSocketFactory = (endPoint) => throw new Exception(); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); _ = await Assert.ThrowsAsync(async () => await adbServer.GetStatusAsync()); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncAlreadyRunningTest() { @@ -77,6 +89,9 @@ public async void StartServerAsyncAlreadyRunningTest() Assert.Equal("host:version", socket.Requests[0]); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncOutdatedRunningNoExecutableTest() { @@ -86,17 +101,22 @@ public async void StartServerAsyncOutdatedRunningNoExecutableTest() _ = await Assert.ThrowsAsync(async () => await adbServer.StartServerAsync(null, false)); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncNotRunningNoExecutableTest() { adbSocketFactory = (endPoint) => throw new SocketException(AdbServer.ConnectionRefused); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); _ = await Assert.ThrowsAsync(async () => await adbServer.StartServerAsync(null, false)); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncOutdatedRunningTest() { @@ -115,13 +135,15 @@ public async void StartServerAsyncOutdatedRunningTest() Assert.Equal("host:kill", socket.Requests[1]); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncNotRunningTest() { adbSocketFactory = (endPoint) => throw new SocketException(AdbServer.ConnectionRefused); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); commandLineClient.Version = new Version(1, 0, 32); @@ -132,6 +154,9 @@ public async void StartServerAsyncNotRunningTest() Assert.True(commandLineClient.ServerStarted); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncIntermediateRestartRequestedRunningTest() { @@ -150,6 +175,9 @@ public async void StartServerAsyncIntermediateRestartRequestedRunningTest() Assert.Equal("host:kill", socket.Requests[1]); } + /// + /// Tests the method. + /// [Fact] public async void StartServerAsyncIntermediateRestartNotRequestedRunningTest() { @@ -167,6 +195,9 @@ public async void StartServerAsyncIntermediateRestartNotRequestedRunningTest() Assert.Equal("host:version", socket.Requests[0]); } + /// + /// Tests the method. + /// [Fact] public async void RestartServerAsyncTest() { @@ -178,10 +209,23 @@ public async void RestartServerAsyncTest() Assert.False(commandLineClient.ServerStarted); _ = await adbServer.RestartServerAsync(ServerName); - Assert.False(commandLineClient.ServerStarted); + Assert.True(commandLineClient.ServerStarted); - Assert.Single(socket.Requests); + Assert.Equal(2, socket.Requests.Count); Assert.Equal("host:version", socket.Requests[0]); + Assert.Equal("host:kill", socket.Requests[1]); + } + + /// + /// Tests the method. + /// + [Fact] + public async void StopServerAsyncTest() + { + await adbServer.StopServerAsync(); + + Assert.Single(socket.Requests); + Assert.Equal("host:kill", socket.Requests[0]); } } } diff --git a/AdvancedSharpAdbClient.Tests/AdbServerTests.cs b/AdvancedSharpAdbClient.Tests/AdbServerTests.cs index 8aaadb3d..6a6fa61d 100644 --- a/AdvancedSharpAdbClient.Tests/AdbServerTests.cs +++ b/AdvancedSharpAdbClient.Tests/AdbServerTests.cs @@ -1,6 +1,4 @@ -using AdvancedSharpAdbClient.Exceptions; -using NSubstitute; -using NSubstitute.ExceptionExtensions; +using NSubstitute; using System; using System.Net; using System.Net.Sockets; @@ -18,7 +16,6 @@ public partial class AdbServerTests private readonly DummyAdbSocket socket; private readonly DummyAdbCommandLineClient commandLineClient; private Func adbSocketFactory; - private AdbClient adbClient; private AdbServer adbServer; public AdbServerTests() @@ -27,26 +24,30 @@ public AdbServerTests() adbSocketFactory = (endPoint) => socket; commandLineClient = new DummyAdbCommandLineClient(); - AdbServer.IsValidAdbFile = commandLineClient.IsValidAdbFile; adbCommandLineClientFactory = (version) => commandLineClient; - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); } + /// + /// Tests the method. + /// [Fact] public void GetStatusNotRunningTest() { - IAdbClient adbClientMock = Substitute.For(); - adbClientMock.GetAdbVersion().Throws(new SocketException(AdbServer.ConnectionRefused)); + IAdbSocket adbSocketMock = Substitute.For(); + adbSocketMock.When(x => x.SendAdbRequest("host:version")).Do(x => throw new SocketException(AdbServer.ConnectionRefused)); - AdbServer adbServer = new(adbClientMock, adbCommandLineClientFactory); + AdbServer adbServer = new((endPoint) => adbSocketMock, adbCommandLineClientFactory); AdbServerStatus status = adbServer.GetStatus(); Assert.False(status.IsRunning); Assert.Null(status.Version); } + /// + /// Tests the method. + /// [Fact] public void GetStatusRunningTest() { @@ -64,28 +65,35 @@ public void GetStatusRunningTest() Assert.Equal(new Version(1, 0, 32), status.Version); } + /// + /// Tests the method. + /// [Fact] public void GetStatusOtherSocketExceptionTest() { adbSocketFactory = (endPoint) => throw new SocketException(); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); _ = Assert.Throws(() => adbServer.GetStatus()); } + /// + /// Tests the method. + /// [Fact] public void GetStatusOtherExceptionTest() { adbSocketFactory = (endPoint) => throw new Exception(); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); _ = Assert.Throws(() => adbServer.GetStatus()); } + /// + /// Tests the method. + /// [Fact] public void StartServerAlreadyRunningTest() { @@ -101,6 +109,9 @@ public void StartServerAlreadyRunningTest() Assert.Equal("host:version", socket.Requests[0]); } + /// + /// Tests the method. + /// [Fact] public void StartServerOutdatedRunningNoExecutableTest() { @@ -110,17 +121,22 @@ public void StartServerOutdatedRunningNoExecutableTest() _ = Assert.Throws(() => adbServer.StartServer(null, false)); } + /// + /// Tests the method. + /// [Fact] public void StartServerNotRunningNoExecutableTest() { adbSocketFactory = (endPoint) => throw new SocketException(AdbServer.ConnectionRefused); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); _ = Assert.Throws(() => adbServer.StartServer(null, false)); } + /// + /// Tests the method. + /// [Fact] public void StartServerOutdatedRunningTest() { @@ -139,13 +155,15 @@ public void StartServerOutdatedRunningTest() Assert.Equal("host:kill", socket.Requests[1]); } + /// + /// Tests the method. + /// [Fact] public void StartServerNotRunningTest() { adbSocketFactory = (endPoint) => throw new SocketException(AdbServer.ConnectionRefused); - adbClient = new AdbClient(AdbClient.DefaultEndPoint, adbSocketFactory); - adbServer = new AdbServer(adbClient, adbCommandLineClientFactory); + adbServer = new AdbServer(adbSocketFactory, adbCommandLineClientFactory); commandLineClient.Version = new Version(1, 0, 32); @@ -156,6 +174,9 @@ public void StartServerNotRunningTest() Assert.True(commandLineClient.ServerStarted); } + /// + /// Tests the method. + /// [Fact] public void StartServerIntermediateRestartRequestedRunningTest() { @@ -174,6 +195,9 @@ public void StartServerIntermediateRestartRequestedRunningTest() Assert.Equal("host:kill", socket.Requests[1]); } + /// + /// Tests the method. + /// [Fact] public void StartServerIntermediateRestartNotRequestedRunningTest() { @@ -191,6 +215,9 @@ public void StartServerIntermediateRestartNotRequestedRunningTest() Assert.Equal("host:version", socket.Requests[0]); } + /// + /// Tests the method. + /// [Fact] public void RestartServerTest() { @@ -202,15 +229,31 @@ public void RestartServerTest() Assert.False(commandLineClient.ServerStarted); _ = adbServer.RestartServer(ServerName); - Assert.False(commandLineClient.ServerStarted); + Assert.True(commandLineClient.ServerStarted); - Assert.Single(socket.Requests); + Assert.Equal(2, socket.Requests.Count); Assert.Equal("host:version", socket.Requests[0]); + Assert.Equal("host:kill", socket.Requests[1]); + } + + /// + /// Tests the method. + /// + [Fact] + public void StopServerTest() + { + adbServer.StopServer(); + + Assert.Single(socket.Requests); + Assert.Equal("host:kill", socket.Requests[0]); } + /// + /// Tests the method. + /// [Fact] public void ConstructorAdbClientNullTest() => - _ = Assert.Throws(() => new AdbServer(null, adbCommandLineClientFactory)); + _ = Assert.Throws(() => new AdbServer((EndPoint)null, adbSocketFactory, adbCommandLineClientFactory)); private static string ServerName => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "adb.exe" : "adb"; } diff --git a/AdvancedSharpAdbClient.Tests/AdbSocketTests.Async.cs b/AdvancedSharpAdbClient.Tests/AdbSocketTests.Async.cs index 8d650767..8d2a6697 100644 --- a/AdvancedSharpAdbClient.Tests/AdbSocketTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/AdbSocketTests.Async.cs @@ -1,5 +1,4 @@ -using AdvancedSharpAdbClient.Exceptions; -using System; +using System; using System.IO; using System.Text; using System.Threading; @@ -10,33 +9,48 @@ namespace AdvancedSharpAdbClient.Tests { public partial class AdbSocketTests { + /// + /// Tests the method. + /// [Fact] public async void SendSyncDATARequestAsyncTest() => await RunTestAsync( - (socket) => socket.SendSyncRequestAsync(SyncCommand.DATA, 2, CancellationToken.None), - new byte[] { (byte)'D', (byte)'A', (byte)'T', (byte)'A', 2, 0, 0, 0 }); + (socket) => socket.SendSyncRequestAsync(SyncCommand.DATA, 2, default), + [(byte)'D', (byte)'A', (byte)'T', (byte)'A', 2, 0, 0, 0]); + /// + /// Tests the method. + /// [Fact] public async void SendSyncSENDRequestAsyncTest() => await RunTestAsync( - (socket) => socket.SendSyncRequestAsync(SyncCommand.SEND, "/test", CancellationToken.None), - new byte[] { (byte)'S', (byte)'E', (byte)'N', (byte)'D', 5, 0, 0, 0, (byte)'/', (byte)'t', (byte)'e', (byte)'s', (byte)'t' }); + (socket) => socket.SendSyncRequestAsync(SyncCommand.SEND, "/test", default), + [(byte)'S', (byte)'E', (byte)'N', (byte)'D', 5, 0, 0, 0, (byte)'/', (byte)'t', (byte)'e', (byte)'s', (byte)'t']); + /// + /// Tests the method. + /// [Fact] public async void SendSyncDENTRequestAsyncTest() => await RunTestAsync( - (socket) => socket.SendSyncRequestAsync(SyncCommand.DENT, "/data", 633, CancellationToken.None), - new byte[] { (byte)'D', (byte)'E', (byte)'N', (byte)'T', 9, 0, 0, 0, (byte)'/', (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)',', (byte)'6', (byte)'3', (byte)'3' }); + (socket) => socket.SendSyncRequestAsync(SyncCommand.DENT, "/data", 633, default), + [(byte)'D', (byte)'E', (byte)'N', (byte)'T', 9, 0, 0, 0, (byte)'/', (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)',', (byte)'6', (byte)'3', (byte)'3']); + /// + /// Tests the method. + /// [Fact] public async void SendSyncNullRequestAsyncTest() => - _ = await Assert.ThrowsAsync(() => RunTestAsync((socket) => socket.SendSyncRequestAsync(SyncCommand.DATA, null, CancellationToken.None), Array.Empty())); + _ = await Assert.ThrowsAsync(() => RunTestAsync((socket) => socket.SendSyncRequestAsync(SyncCommand.DATA, null, default), [])); + /// + /// Tests the method. + /// [Fact] public async void ReadSyncResponseAsync() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); await using (StreamWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, 4, true)) { @@ -48,11 +62,14 @@ public async void ReadSyncResponseAsync() Assert.Equal(SyncCommand.DENT, await socket.ReadSyncResponseAsync()); } + /// + /// Tests the method. + /// [Fact] public async void ReadStringAsyncTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); await using (BinaryWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, true)) { @@ -66,11 +83,14 @@ public async void ReadStringAsyncTest() Assert.Equal("Hello", await socket.ReadStringAsync()); } + /// + /// Tests the method. + /// [Fact] public async void ReadAdbOkayResponseAsyncTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); await using (StreamWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, 4, true)) { @@ -86,11 +106,14 @@ public async void ReadAdbOkayResponseAsyncTest() Assert.False(response.Timeout); } + /// + /// Tests the method. + /// [Fact] public async void ReadAdbFailResponseAsyncTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); await using (StreamWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, 4, true)) { @@ -104,11 +127,14 @@ public async void ReadAdbFailResponseAsyncTest() _ = await Assert.ThrowsAsync(() => socket.ReadAdbResponseAsync()); } + /// + /// Tests the method. + /// [Fact] public async void ReadAsyncTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); // Read 100 bytes from a stream which has 101 bytes available byte[] data = new byte[101]; @@ -117,13 +143,13 @@ public async void ReadAsyncTest() data[i] = (byte)i; } - await tcpSocket.InputStream.WriteAsync(data, 0, 101); + await tcpSocket.InputStream.WriteAsync(data); tcpSocket.InputStream.Position = 0; // Buffer has a capacity of 101, but we'll only want to read 100 bytes byte[] received = new byte[101]; - await socket.ReadAsync(received, 100, CancellationToken.None); + await socket.ReadAsync(received, 100); for (int i = 0; i < 100; i++) { @@ -133,16 +159,51 @@ public async void ReadAsyncTest() Assert.Equal(0, received[100]); } + /// + /// Tests the method. + /// + [Fact] + public async void ReadAsyncMemoryTest() + { + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); + + // Read 100 bytes from a stream which has 101 bytes available + byte[] data = new byte[101]; + for (int i = 0; i < 101; i++) + { + data[i] = (byte)i; + } + + await tcpSocket.InputStream.WriteAsync(data); + tcpSocket.InputStream.Position = 0; + + // Buffer has a capacity of 101, but we'll only want to read 100 bytes + byte[] received = new byte[101]; + + await socket.ReadAsync(received.AsMemory(0, 100)); + + for (int i = 0; i < 100; i++) + { + Assert.Equal(received[i], (byte)i); + } + + Assert.Equal(0, received[100]); + } + + /// + /// Tests the method. + /// [Fact] public async void SendAdbRequestAsyncTest() => await RunTestAsync( - (socket) => socket.SendAdbRequestAsync("Test", CancellationToken.None), + (socket) => socket.SendAdbRequestAsync("Test", default), Encoding.ASCII.GetBytes("0004Test")); private static async Task RunTestAsync(Func test, byte[] expectedDataSent) { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); // Run the test. await test(socket); diff --git a/AdvancedSharpAdbClient.Tests/AdbSocketTests.cs b/AdvancedSharpAdbClient.Tests/AdbSocketTests.cs index a6b11ba9..b98f2ec4 100644 --- a/AdvancedSharpAdbClient.Tests/AdbSocketTests.cs +++ b/AdvancedSharpAdbClient.Tests/AdbSocketTests.cs @@ -1,6 +1,4 @@ -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; -using System; +using System; using System.IO; using System.Text; using Xunit; @@ -12,23 +10,29 @@ namespace AdvancedSharpAdbClient.Tests /// public partial class AdbSocketTests { + /// + /// Tests the method. + /// [Fact] public void CloseTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); Assert.True(socket.Connected); - socket.Dispose(); + socket.Close(); Assert.False(socket.Connected); } + /// + /// Tests the method. + /// [Fact] public void DisposeTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); Assert.True(socket.Connected); @@ -36,6 +40,9 @@ public void DisposeTest() Assert.False(socket.Connected); } + /// + /// Tests the method. + /// [Fact] public void IsOkayTest() { @@ -46,33 +53,48 @@ public void IsOkayTest() Assert.False(AdbSocket.IsOkay(fail)); } + /// + /// Tests the method. + /// [Fact] public void SendSyncDATARequestTest() => RunTest( (socket) => socket.SendSyncRequest(SyncCommand.DATA, 2), - new byte[] { (byte)'D', (byte)'A', (byte)'T', (byte)'A', 2, 0, 0, 0 }); + [(byte)'D', (byte)'A', (byte)'T', (byte)'A', 2, 0, 0, 0]); + /// + /// Tests the method. + /// [Fact] public void SendSyncSENDRequestTest() => RunTest( (socket) => socket.SendSyncRequest(SyncCommand.SEND, "/test"), - new byte[] { (byte)'S', (byte)'E', (byte)'N', (byte)'D', 5, 0, 0, 0, (byte)'/', (byte)'t', (byte)'e', (byte)'s', (byte)'t' }); + [(byte)'S', (byte)'E', (byte)'N', (byte)'D', 5, 0, 0, 0, (byte)'/', (byte)'t', (byte)'e', (byte)'s', (byte)'t']); + /// + /// Tests the method. + /// [Fact] public void SendSyncDENTRequestTest() => RunTest( (socket) => socket.SendSyncRequest(SyncCommand.DENT, "/data", 633), - new byte[] { (byte)'D', (byte)'E', (byte)'N', (byte)'T', 9, 0, 0, 0, (byte)'/', (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)',', (byte)'6', (byte)'3', (byte)'3' }); + [(byte)'D', (byte)'E', (byte)'N', (byte)'T', 9, 0, 0, 0, (byte)'/', (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)',', (byte)'6', (byte)'3', (byte)'3']); + /// + /// Tests the method. + /// [Fact] public void SendSyncNullRequestTest() => - _ = Assert.Throws(() => RunTest((socket) => socket.SendSyncRequest(SyncCommand.DATA, null), Array.Empty())); + _ = Assert.Throws(() => RunTest((socket) => socket.SendSyncRequest(SyncCommand.DATA, null), [])); + /// + /// Tests the method. + /// [Fact] - public void ReadSyncResponse() + public void ReadSyncResponseTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); using (StreamWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, 4, true)) { @@ -84,11 +106,14 @@ public void ReadSyncResponse() Assert.Equal(SyncCommand.DENT, socket.ReadSyncResponse()); } + /// + /// Tests the method. + /// [Fact] - public void ReadSyncString() + public void ReadSyncStringTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); using (BinaryWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, true)) { @@ -102,11 +127,14 @@ public void ReadSyncString() Assert.Equal("Hello", socket.ReadSyncString()); } + /// + /// Tests the method. + /// [Fact] public void ReadAdbOkayResponseTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); using (StreamWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, 4, true)) { @@ -122,11 +150,14 @@ public void ReadAdbOkayResponseTest() Assert.False(response.Timeout); } + /// + /// Tests the method. + /// [Fact] public void ReadAdbFailResponseTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); using (StreamWriter writer = new(tcpSocket.InputStream, Encoding.ASCII, 4, true)) { @@ -137,14 +168,17 @@ public void ReadAdbFailResponseTest() tcpSocket.InputStream.Position = 0; - _ = Assert.Throws(socket.ReadAdbResponse); + _ = Assert.Throws(() => _ = socket.ReadAdbResponse()); } + /// + /// Tests the method. + /// [Fact] public void ReadTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); // Read 100 bytes from a stream which has 101 bytes available byte[] data = new byte[101]; @@ -153,7 +187,7 @@ public void ReadTest() data[i] = (byte)i; } - tcpSocket.InputStream.Write(data, 0, 101); + tcpSocket.InputStream.Write(data); tcpSocket.InputStream.Position = 0; // Buffer has a capacity of 101, but we'll only want to read 100 bytes @@ -169,29 +203,67 @@ public void ReadTest() Assert.Equal(0, received[100]); } + /// + /// Tests the method. + /// + [Fact] + public void ReadSpanTest() + { + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); + + // Read 100 bytes from a stream which has 101 bytes available + byte[] data = new byte[101]; + for (int i = 0; i < 101; i++) + { + data[i] = (byte)i; + } + + tcpSocket.InputStream.Write(data); + tcpSocket.InputStream.Position = 0; + + // Buffer has a capacity of 101, but we'll only want to read 100 bytes + byte[] received = new byte[101]; + + _ = socket.Read(received.AsSpan(0, 100)); + + for (int i = 0; i < 100; i++) + { + Assert.Equal(received[i], (byte)i); + } + + Assert.Equal(0, received[100]); + } + + /// + /// Tests the method. + /// [Fact] public void SendAdbRequestTest() => RunTest( (socket) => socket.SendAdbRequest("Test"), Encoding.ASCII.GetBytes("0004Test")); + /// + /// Tests the method. + /// [Fact] public void GetShellStreamTest() { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); - Stream stream = socket.GetShellStream(); + using Stream stream = socket.GetShellStream(); Assert.IsType(stream); - ShellStream shellStream = (ShellStream)stream; + using ShellStream shellStream = (ShellStream)stream; Assert.Equal(tcpSocket.OutputStream, shellStream.Inner); } private static void RunTest(Action test, byte[] expectedDataSent) { - DummyTcpSocket tcpSocket = new(); - AdbSocket socket = new(tcpSocket); + using DummyTcpSocket tcpSocket = new(); + using AdbSocket socket = new(tcpSocket); // Run the test. test(socket); diff --git a/AdvancedSharpAdbClient.Tests/AdvancedSharpAdbClient.Tests.csproj b/AdvancedSharpAdbClient.Tests/AdvancedSharpAdbClient.Tests.csproj index 1589ea42..c11c4348 100644 --- a/AdvancedSharpAdbClient.Tests/AdvancedSharpAdbClient.Tests.csproj +++ b/AdvancedSharpAdbClient.Tests/AdvancedSharpAdbClient.Tests.csproj @@ -3,16 +3,17 @@ latest CS1591 - net6.0 + net8.0 + net8.0-windows10.0.17763.0 runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,55 +24,7 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.Async.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.Async.cs index 8d5487a4..e07e8b09 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.Async.cs @@ -1,7 +1,5 @@ -using AdvancedSharpAdbClient.Tests; -using NSubstitute; +using NSubstitute; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -21,12 +19,15 @@ public async void StatAsyncTest() ISyncService mock = Substitute.For(); mock.StatAsync("/test", Arg.Any()).Returns(tcs.Task); - Factories.SyncServiceFactory = (c, d) => mock; - DeviceData device = new(); - Assert.Equal(await tcs.Task, await client.StatAsync(device, "/test")); - Factories.Reset(); + Factories.SyncServiceFactory = (c, d) => + { + Factories.Reset(); + return mock; + }; + + Assert.Equal(await tcs.Task, await client.StatAsync(device, "/test")); } [Fact] @@ -34,7 +35,7 @@ public async void GetEnvironmentVariablesAsyncTest() { DummyAdbClient adbClient = new(); - adbClient.Commands[EnvironmentVariablesReceiver.PrintEnvCommand] = "a=b"; + adbClient.Commands[$"shell:{EnvironmentVariablesReceiver.PrintEnvCommand}"] = "a=b"; DeviceData device = new(); @@ -50,8 +51,8 @@ public async void UninstallPackageAsyncTests() { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm uninstall com.example"] = "Success"; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm uninstall com.example"] = "Success"; DeviceData device = new() { @@ -60,8 +61,8 @@ public async void UninstallPackageAsyncTests() await adbClient.UninstallPackageAsync(device, "com.example"); Assert.Equal(2, adbClient.ReceivedCommands.Count); - Assert.Equal("pm list packages -f", adbClient.ReceivedCommands[0]); - Assert.Equal("pm uninstall com.example", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm list packages -f", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm uninstall com.example", adbClient.ReceivedCommands[1]); } [Theory] @@ -295,8 +296,8 @@ public async void GetPackageVersionAsyncTest(string command, int versionCode, st { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands[$"dumpsys package {packageName}"] = command; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands[$"shell:dumpsys package {packageName}"] = command; DeviceData device = new() { @@ -308,8 +309,8 @@ public async void GetPackageVersionAsyncTest(string command, int versionCode, st Assert.Equal(versionName, version.VersionName); Assert.Equal(2, adbClient.ReceivedCommands.Count); - Assert.Equal("pm list packages -f", adbClient.ReceivedCommands[0]); - Assert.Equal($"dumpsys package {packageName}", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm list packages -f", adbClient.ReceivedCommands[0]); + Assert.Equal($"shell:dumpsys package {packageName}", adbClient.ReceivedCommands[1]); } [Fact] @@ -317,7 +318,7 @@ public async void ListProcessesAsyncTest() { DummyAdbClient adbClient = new(); - adbClient.Commands[@"SDK=""$(/system/bin/getprop ro.build.version.sdk)"" + adbClient.Commands[@"shell:SDK=""$(/system/bin/getprop ro.build.version.sdk)"" if [ $SDK -lt 24 ] then /system/bin/ls /proc/ @@ -329,11 +330,11 @@ public async void ListProcessesAsyncTest() 3 acpi asound"; - adbClient.Commands["cat /proc/1/stat /proc/2/stat /proc/3/stat "] = + adbClient.Commands["shell:cat /proc/1/stat /proc/2/stat /proc/3/stat"] = @"1 (init) S 0 0 0 0 -1 1077944576 2680 83280 0 179 0 67 16 39 20 0 1 0 2 17735680 143 18446744073709551615 134512640 135145076 4288071392 4288070744 134658736 0 0 0 65536 18446744071580117077 0 0 17 1 0 0 0 0 0 135152736 135165080 142131200 4288073690 4288073696 4288073696 4288073714 0 2 (kthreadd) S 0 0 0 0 -1 2129984 0 0 0 0 0 0 0 0 20 0 1 0 2 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 18446744071579254310 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 (ksoftirqd/0) S 2 0 0 0 -1 69238848 0 0 0 0 0 23 0 0 20 0 1 0 7 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 18446744071579284070 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0 0 0"; - adbClient.Commands["cat /proc/1/cmdline /proc/1/stat /proc/2/cmdline /proc/2/stat /proc/3/cmdline /proc/3/stat "] = + adbClient.Commands["shell:cat /proc/1/cmdline /proc/1/stat /proc/2/cmdline /proc/2/stat /proc/3/cmdline /proc/3/stat"] = @" 1 (init) S 0 0 0 0 -1 1077944576 2680 83280 0 179 0 67 16 39 20 0 1 0 2 17735680 143 18446744073709551615 134512640 135145076 4288071392 4288070744 134658736 0 0 0 65536 18446744071580117077 0 0 17 1 0 0 0 0 0 135152736 135165080 142131200 4288073690 4288073696 4288073696 4288073714 0 @@ -342,7 +343,7 @@ public async void ListProcessesAsyncTest() 3 (ksoftirqd/0) S 2 0 0 0 -1 69238848 0 0 0 0 0 23 0 0 20 0 1 0 7 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 18446744071579284070 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0 0 0"; DeviceData device = new(); - AndroidProcess[] processes = (await adbClient.ListProcessesAsync(device)).ToArray(); + AndroidProcess[] processes = await adbClient.ListProcessesAsync(device).ToArrayAsync(); Assert.Equal(3, processes.Length); Assert.Equal("init", processes[0].Name); diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.cs index 1d4978c7..d96a0bcf 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/DeviceExtensionsTests.cs @@ -1,5 +1,4 @@ -using AdvancedSharpAdbClient.Tests; -using NSubstitute; +using NSubstitute; using System.Collections.Generic; using System.Linq; using Xunit; @@ -20,12 +19,15 @@ public void StatTest() ISyncService mock = Substitute.For(); mock.Stat("/test").Returns(stats); - Factories.SyncServiceFactory = (c, d) => mock; - DeviceData device = new(); - Assert.Equal(stats, client.Stat(device, "/test")); - Factories.Reset(); + Factories.SyncServiceFactory = (c, d) => + { + Factories.Reset(); + return mock; + }; + + Assert.Equal(stats, client.Stat(device, "/test")); } [Fact] @@ -33,7 +35,7 @@ public void GetEnvironmentVariablesTest() { DummyAdbClient adbClient = new(); - adbClient.Commands[EnvironmentVariablesReceiver.PrintEnvCommand] = "a=b"; + adbClient.Commands[$"shell:{EnvironmentVariablesReceiver.PrintEnvCommand}"] = "a=b"; DeviceData device = new(); @@ -49,8 +51,8 @@ public void UninstallPackageTests() { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm uninstall com.example"] = "Success"; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm uninstall com.example"] = "Success"; DeviceData device = new() { @@ -59,8 +61,8 @@ public void UninstallPackageTests() adbClient.UninstallPackage(device, "com.example"); Assert.Equal(2, adbClient.ReceivedCommands.Count); - Assert.Equal("pm list packages -f", adbClient.ReceivedCommands[0]); - Assert.Equal("pm uninstall com.example", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm list packages -f", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm uninstall com.example", adbClient.ReceivedCommands[1]); } [Theory] @@ -294,8 +296,8 @@ public void GetPackageVersionTest(string command, int versionCode, string versio { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands[$"dumpsys package {packageName}"] = command; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands[$"shell:dumpsys package {packageName}"] = command; DeviceData device = new() { @@ -307,8 +309,8 @@ public void GetPackageVersionTest(string command, int versionCode, string versio Assert.Equal(versionName, version.VersionName); Assert.Equal(2, adbClient.ReceivedCommands.Count); - Assert.Equal("pm list packages -f", adbClient.ReceivedCommands[0]); - Assert.Equal($"dumpsys package {packageName}", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm list packages -f", adbClient.ReceivedCommands[0]); + Assert.Equal($"shell:dumpsys package {packageName}", adbClient.ReceivedCommands[1]); } [Fact] @@ -316,7 +318,7 @@ public void ListProcessesTest() { DummyAdbClient adbClient = new(); - adbClient.Commands[@"SDK=""$(/system/bin/getprop ro.build.version.sdk)"" + adbClient.Commands[@"shell:SDK=""$(/system/bin/getprop ro.build.version.sdk)"" if [ $SDK -lt 24 ] then /system/bin/ls /proc/ @@ -328,11 +330,11 @@ public void ListProcessesTest() 3 acpi asound"; - adbClient.Commands["cat /proc/1/stat /proc/2/stat /proc/3/stat "] = + adbClient.Commands["shell:cat /proc/1/stat /proc/2/stat /proc/3/stat"] = @"1 (init) S 0 0 0 0 -1 1077944576 2680 83280 0 179 0 67 16 39 20 0 1 0 2 17735680 143 18446744073709551615 134512640 135145076 4288071392 4288070744 134658736 0 0 0 65536 18446744071580117077 0 0 17 1 0 0 0 0 0 135152736 135165080 142131200 4288073690 4288073696 4288073696 4288073714 0 2 (kthreadd) S 0 0 0 0 -1 2129984 0 0 0 0 0 0 0 0 20 0 1 0 2 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 18446744071579254310 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 (ksoftirqd/0) S 2 0 0 0 -1 69238848 0 0 0 0 0 23 0 0 20 0 1 0 7 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 18446744071579284070 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0 0 0"; - adbClient.Commands["cat /proc/1/cmdline /proc/1/stat /proc/2/cmdline /proc/2/stat /proc/3/cmdline /proc/3/stat "] = + adbClient.Commands["shell:cat /proc/1/cmdline /proc/1/stat /proc/2/cmdline /proc/2/stat /proc/3/cmdline /proc/3/stat"] = @" 1 (init) S 0 0 0 0 -1 1077944576 2680 83280 0 179 0 67 16 39 20 0 1 0 2 17735680 143 18446744073709551615 134512640 135145076 4288071392 4288070744 134658736 0 0 0 65536 18446744071580117077 0 0 17 1 0 0 0 0 0 135152736 135165080 142131200 4288073690 4288073696 4288073696 4288073714 0 diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/AndroidProcessTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/AndroidProcessTests.cs index 5749e36a..99b8e753 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/AndroidProcessTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/AndroidProcessTests.cs @@ -7,7 +7,7 @@ using System; using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Models.DeviceCommands.Tests { /// /// Tests the class. diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/VersionInfoTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/VersionInfoTests.cs index 9582f81c..3d93c4fa 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/VersionInfoTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Models/VersionInfoTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Models.DeviceCommands.Tests { /// /// Tests the class. @@ -26,8 +26,8 @@ public void DeconstructTest(int versionCode, string versionName) [Fact] public void ToStringTest() { - VersionInfo v = new(1234, "1.2.3.4"); - Assert.Equal("1.2.3.4 (1234)", v.ToString()); + VersionInfo version = new(1234, "1.2.3.4"); + Assert.Equal("1.2.3.4 (1234)", version.ToString()); } } } diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.Async.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.Async.cs index 209b8f2c..80672c78 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.Async.cs @@ -1,5 +1,4 @@ -using AdvancedSharpAdbClient.Tests; -using System.IO; +using System.IO; using Xunit; namespace AdvancedSharpAdbClient.DeviceCommands.Tests @@ -11,9 +10,9 @@ public async void InstallRemotePackageAsyncTest() { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install \"/data/test.apk\""] = "Success"; - adbClient.Commands["pm install -r \"/data/test.apk\""] = "Success"; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install \"/data/test.apk\""] = "Success"; + adbClient.Commands["shell:pm install -r -t \"/data/test.apk\""] = "Success"; DeviceData device = new() { @@ -21,15 +20,24 @@ public async void InstallRemotePackageAsyncTest() }; PackageManager manager = new(adbClient, device); - await manager.InstallRemotePackageAsync("/data/test.apk", false); + + using (EventTestHost eventTestHost = new(manager, PackageInstallProgressState.Installing)) + { + await manager.InstallRemotePackageAsync("/data/test.apk"); + } Assert.Equal(2, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install \"/data/test.apk\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install \"/data/test.apk\"", adbClient.ReceivedCommands[1]); - await manager.InstallRemotePackageAsync("/data/test.apk", true); + adbClient.ReceivedCommands.Clear(); - Assert.Equal(3, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install -r \"/data/test.apk\"", adbClient.ReceivedCommands[2]); + using (EventTestHost eventTestHost = new(manager, PackageInstallProgressState.Installing)) + { + await manager.InstallRemotePackageAsync("/data/test.apk", "-r", "-t"); + } + + Assert.Single(adbClient.ReceivedCommands); + Assert.Equal("shell:pm install -r -t \"/data/test.apk\"", adbClient.ReceivedCommands[0]); } [Fact] @@ -37,24 +45,32 @@ public async void InstallPackageAsyncTest() { DummySyncService syncService = new(); - Factories.SyncServiceFactory = (c, d) => syncService; - DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install \"/data/local/tmp/test.txt\""] = "Success"; - adbClient.Commands["rm \"/data/local/tmp/test.txt\""] = string.Empty; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install \"/data/local/tmp/test.txt\""] = "Success"; + adbClient.Commands["shell:rm \"/data/local/tmp/test.txt\""] = string.Empty; DeviceData device = new() { State = DeviceState.Online }; - PackageManager manager = new(adbClient, device); - await manager.InstallPackageAsync("Assets/test.txt", false); + PackageManager manager = new(adbClient, device, (c, d) => syncService); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.Uploading, + PackageInstallProgressState.Installing, + PackageInstallProgressState.PostInstall, + PackageInstallProgressState.Finished)) + { + await manager.InstallPackageAsync("Assets/test.txt"); + } + Assert.Equal(3, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[1]); - Assert.Equal("rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); Assert.Single(syncService.UploadedFiles); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/test.txt")); @@ -67,15 +83,15 @@ public async void InstallMultipleRemotePackageAsyncTest() { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install-create"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -r"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -r -p com.google.android.gms"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-write 936013062 base.apk \"/data/base.apk\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\""] = "Success"; - adbClient.Commands["pm install-commit 936013062"] = "Success"; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install-create"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -r -t"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -p com.google.android.gms -r -t"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-write 936013062 base.apk \"/data/base.apk\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\""] = "Success"; + adbClient.Commands["shell:pm install-commit 936013062"] = "Success"; DeviceData device = new() { @@ -83,39 +99,74 @@ public async void InstallMultipleRemotePackageAsyncTest() }; PackageManager manager = new(adbClient, device); - await manager.InstallMultipleRemotePackageAsync("/data/base.apk", new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, false); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + await manager.InstallMultipleRemotePackageAsync("/data/base.apk", ["/data/split-dpi.apk", "/data/split-abi.apk"]); + } Assert.Equal(6, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create", adbClient.ReceivedCommands[1]); - Assert.Equal("pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[2]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[3]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[4]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[5]); - - await manager.InstallMultipleRemotePackageAsync("/data/base.apk", new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, true); - - Assert.Equal(11, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -r", adbClient.ReceivedCommands[6]); - Assert.Equal("pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[7]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[8]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[9]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[10]); - - await manager.InstallMultipleRemotePackageAsync(new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, "com.google.android.gms", false); - - Assert.Equal(15, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[11]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[12]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[13]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[14]); - - await manager.InstallMultipleRemotePackageAsync(new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, "com.google.android.gms", true); - - Assert.Equal(19, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -r -p com.google.android.gms", adbClient.ReceivedCommands[15]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[16]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[17]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[18]); + Assert.Equal("shell:pm install-create", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[2]); + Assert.Contains("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[3..5]); + Assert.Contains("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[3..5]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[5]); + + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + await manager.InstallMultipleRemotePackageAsync("/data/base.apk", ["/data/split-dpi.apk", "/data/split-abi.apk"], "-r", "-t"); + } + + Assert.Equal(5, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -r -t", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[1]); + Assert.Contains("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[2..4]); + Assert.Contains("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[2..4]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[4]); + + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + await manager.InstallMultipleRemotePackageAsync(["/data/split-dpi.apk", "/data/split-abi.apk"], "com.google.android.gms"); + } + + Assert.Equal(4, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[0]); + Assert.Contains("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[1..3]); + Assert.Contains("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[1..3]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[3]); + + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + await manager.InstallMultipleRemotePackageAsync(["/data/split-dpi.apk", "/data/split-abi.apk"], "com.google.android.gms", "-r", "-t"); + } + + Assert.Equal(4, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -p com.google.android.gms -r -t", adbClient.ReceivedCommands[0]); + Assert.Contains("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[1..3]); + Assert.Contains("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[1..3]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[3]); } [Fact] @@ -123,37 +174,47 @@ public async void InstallMultiplePackageAsyncTest() { DummySyncService syncService = new(); - Factories.SyncServiceFactory = (c, d) => syncService; - DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install-create"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp0.apk \"/data/local/tmp/gapps.txt\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp1.apk \"/data/local/tmp/logcat.bin\""] = "Success"; - adbClient.Commands["pm install-commit 936013062"] = "Success"; - adbClient.Commands["rm \"/data/local/tmp/test.txt\""] = string.Empty; - adbClient.Commands["rm \"/data/local/tmp/gapps.txt\""] = string.Empty; - adbClient.Commands["rm \"/data/local/tmp/logcat.bin\""] = string.Empty; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install-create"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split0.apk \"/data/local/tmp/gapps.txt\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split1.apk \"/data/local/tmp/logcat.bin\""] = "Success"; + adbClient.Commands["shell:pm install-commit 936013062"] = "Success"; + adbClient.Commands["shell:rm \"/data/local/tmp/test.txt\""] = string.Empty; + adbClient.Commands["shell:rm \"/data/local/tmp/gapps.txt\""] = string.Empty; + adbClient.Commands["shell:rm \"/data/local/tmp/logcat.bin\""] = string.Empty; DeviceData device = new() { State = DeviceState.Online }; - PackageManager manager = new(adbClient, device); - await manager.InstallMultiplePackageAsync("Assets/test.txt", new string[] { "Assets/gapps.txt", "Assets/logcat.bin" }, false); + PackageManager manager = new(adbClient, device, (c, d) => syncService); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.Uploading, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing, + PackageInstallProgressState.PostInstall, + PackageInstallProgressState.Finished)) + { + await manager.InstallMultiplePackageAsync("Assets/test.txt", ["Assets/gapps.txt", "Assets/logcat.bin"]); + } + Assert.Equal(9, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create", adbClient.ReceivedCommands[1]); - Assert.Equal("pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[3]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[4]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[5]); - Assert.Equal("rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[6]); - Assert.Equal("rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[7]); - Assert.Equal("rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[8]); + Assert.Equal("shell:pm install-create", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); + Assert.Contains("shell:pm install-write 936013062 split0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[3..5]); + Assert.Contains("shell:pm install-write 936013062 split1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[3..5]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[5]); + Assert.Contains("shell:rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[6..8]); + Assert.Contains("shell:rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[6..8]); + Assert.Equal("shell:rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[8]); Assert.Equal(3, syncService.UploadedFiles.Count); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/test.txt")); @@ -161,20 +222,31 @@ public async void InstallMultiplePackageAsyncTest() Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/logcat.bin")); syncService.UploadedFiles.Clear(); - await manager.InstallMultiplePackageAsync(new string[] { "Assets/gapps.txt", "Assets/logcat.bin" }, "com.google.android.gms", false); - Assert.Equal(15, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[9]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[10]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[11]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[12]); - Assert.Equal("rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[6]); - Assert.Equal("rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[7]); + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.Uploading, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing, + PackageInstallProgressState.PostInstall, + PackageInstallProgressState.Finished)) + { + await manager.InstallMultiplePackageAsync(["Assets/gapps.txt", "Assets/logcat.bin"], "com.google.android.gms"); + } + + Assert.Equal(6, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[0]); + Assert.Contains("shell:pm install-write 936013062 split0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[1..3]); + Assert.Contains("shell:pm install-write 936013062 split1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[1..3]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[3]); + Assert.Contains("shell:rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[4..6]); + Assert.Contains("shell:rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[4..6]); Assert.Equal(2, syncService.UploadedFiles.Count); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/gapps.txt")); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/logcat.bin")); - - Factories.Reset(); } [Fact] @@ -186,8 +258,8 @@ public async void UninstallPackageAsyncTest() }; DummyAdbClient client = new(); - client.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - client.Commands["pm uninstall com.android.gallery3d"] = "Success"; + client.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + client.Commands["shell:pm uninstall com.android.gallery3d"] = "Success"; PackageManager manager = new(client, device); // Command should execute correctly; if the wrong command is passed an exception @@ -204,7 +276,7 @@ public async void GetPackageVersionInfoAsyncTest() }; DummyAdbClient client = new(); - client.Commands["dumpsys package com.google.android.gms"] = File.ReadAllText("Assets/gapps.txt"); + client.Commands["shell:dumpsys package com.google.android.gms"] = File.ReadAllText("Assets/gapps.txt"); PackageManager manager = new(client, device, skipInit: true); VersionInfo versionInfo = await manager.GetVersionInfoAsync("com.google.android.gms"); diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.cs index 5e7a5381..ab772950 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/PackageManagerTests.cs @@ -1,12 +1,10 @@ -using AdvancedSharpAdbClient.Tests; -using NSubstitute; +using NSubstitute; using System; using System.IO; using Xunit; namespace AdvancedSharpAdbClient.DeviceCommands.Tests { - public partial class PackageManagerTests { [Fact] @@ -19,7 +17,7 @@ public void ConstructorNullTest() [Theory] [InlineData("package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d", "com.android.gallery3d", "/system/app/Gallery2/Gallery2.apk")] - [InlineData("package:mwc2015.be", "mwc2015.be", null)] + [InlineData("package:mwc2015.be", "mwc2015.be", "")] public void PackagesPropertyTest(string command, string packageName, string path) { DeviceData device = new() @@ -28,7 +26,7 @@ public void PackagesPropertyTest(string command, string packageName, string path }; DummyAdbClient client = new(); - client.Commands["pm list packages -f"] = command; + client.Commands["shell:pm list packages -f"] = command; PackageManager manager = new(client, device); Assert.True(manager.Packages.ContainsKey(packageName)); @@ -40,9 +38,9 @@ public void InstallRemotePackageTest() { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install \"/data/test.apk\""] = "Success"; - adbClient.Commands["pm install -r \"/data/test.apk\""] = "Success"; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install \"/data/test.apk\""] = "Success"; + adbClient.Commands["shell:pm install -r -t \"/data/test.apk\""] = "Success"; DeviceData device = new() { @@ -50,15 +48,24 @@ public void InstallRemotePackageTest() }; PackageManager manager = new(adbClient, device); - manager.InstallRemotePackage("/data/test.apk", false); + + using (EventTestHost eventTestHost = new(manager, PackageInstallProgressState.Installing)) + { + manager.InstallRemotePackage("/data/test.apk"); + } Assert.Equal(2, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install \"/data/test.apk\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install \"/data/test.apk\"", adbClient.ReceivedCommands[1]); - manager.InstallRemotePackage("/data/test.apk", true); + adbClient.ReceivedCommands.Clear(); - Assert.Equal(3, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install -r \"/data/test.apk\"", adbClient.ReceivedCommands[2]); + using (EventTestHost eventTestHost = new(manager, PackageInstallProgressState.Installing)) + { + manager.InstallRemotePackage("/data/test.apk", "-r", "-t"); + } + + Assert.Single(adbClient.ReceivedCommands); + Assert.Equal("shell:pm install -r -t \"/data/test.apk\"", adbClient.ReceivedCommands[0]); } [Fact] @@ -66,29 +73,35 @@ public void InstallPackageTest() { DummySyncService syncService = new(); - Factories.SyncServiceFactory = (c, d) => syncService; - DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install \"/data/local/tmp/test.txt\""] = "Success"; - adbClient.Commands["rm \"/data/local/tmp/test.txt\""] = string.Empty; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install \"/data/local/tmp/test.txt\""] = "Success"; + adbClient.Commands["shell:rm \"/data/local/tmp/test.txt\""] = string.Empty; DeviceData device = new() { State = DeviceState.Online }; - PackageManager manager = new(adbClient, device); - manager.InstallPackage("Assets/test.txt", false); + PackageManager manager = new(adbClient, device, (c, d) => syncService); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.Uploading, + PackageInstallProgressState.Installing, + PackageInstallProgressState.PostInstall, + PackageInstallProgressState.Finished)) + { + manager.InstallPackage("Assets/test.txt"); + } + Assert.Equal(3, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[1]); - Assert.Equal("rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); Assert.Single(syncService.UploadedFiles); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/test.txt")); - - Factories.Reset(); } [Fact] @@ -96,15 +109,15 @@ public void InstallMultipleRemotePackageTest() { DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install-create"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -r"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -r -p com.google.android.gms"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-write 936013062 base.apk \"/data/base.apk\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\""] = "Success"; - adbClient.Commands["pm install-commit 936013062"] = "Success" ; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install-create"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -r -t"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -p com.google.android.gms -r -t"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-write 936013062 base.apk \"/data/base.apk\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\""] = "Success"; + adbClient.Commands["shell:pm install-commit 936013062"] = "Success"; DeviceData device = new() { @@ -112,39 +125,75 @@ public void InstallMultipleRemotePackageTest() }; PackageManager manager = new(adbClient, device); - manager.InstallMultipleRemotePackage("/data/base.apk", new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, false); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + manager.InstallMultipleRemotePackage("/data/base.apk", ["/data/split-dpi.apk", "/data/split-abi.apk"]); + } Assert.Equal(6, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create", adbClient.ReceivedCommands[1]); - Assert.Equal("pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[2]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[3]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[4]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[5]); - - manager.InstallMultipleRemotePackage("/data/base.apk", new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, true); - - Assert.Equal(11, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -r", adbClient.ReceivedCommands[6]); - Assert.Equal("pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[7]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[8]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[9]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[10]); - - manager.InstallMultipleRemotePackage(new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, "com.google.android.gms", false); - - Assert.Equal(15, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[11]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[12]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[13]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[14]); - - manager.InstallMultipleRemotePackage(new string[] { "/data/split-dpi.apk", "/data/split-abi.apk" }, "com.google.android.gms", true); - - Assert.Equal(19, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -r -p com.google.android.gms", adbClient.ReceivedCommands[15]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[16]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[17]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[18]); + Assert.Equal("shell:pm install-create", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[3]); + Assert.Equal("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[4]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[5]); + + adbClient.ReceivedCommands.Clear(); + + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + manager.InstallMultipleRemotePackage("/data/base.apk", ["/data/split-dpi.apk", "/data/split-abi.apk"], "-r", "-t"); + } + + Assert.Equal(5, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -r -t", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm install-write 936013062 base.apk \"/data/base.apk\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[3]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[4]); + + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + manager.InstallMultipleRemotePackage(["/data/split-dpi.apk", "/data/split-abi.apk"], "com.google.android.gms"); + } + + Assert.Equal(4, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[3]); + + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing)) + { + manager.InstallMultipleRemotePackage(["/data/split-dpi.apk", "/data/split-abi.apk"], "com.google.android.gms", "-r", "-t"); + } + + Assert.Equal(4, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -p com.google.android.gms -r -t", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm install-write 936013062 split0.apk \"/data/split-dpi.apk\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 split1.apk \"/data/split-abi.apk\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[3]); } [Fact] @@ -152,37 +201,47 @@ public void InstallMultiplePackageTest() { DummySyncService syncService = new(); - Factories.SyncServiceFactory = (c, d) => syncService; - DummyAdbClient adbClient = new(); - adbClient.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - adbClient.Commands["pm install-create"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; - adbClient.Commands["pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp0.apk \"/data/local/tmp/gapps.txt\""] = "Success"; - adbClient.Commands["pm install-write 936013062 splitapp1.apk \"/data/local/tmp/logcat.bin\""] = "Success"; - adbClient.Commands["pm install-commit 936013062"] = "Success"; - adbClient.Commands["rm \"/data/local/tmp/test.txt\""] = string.Empty; - adbClient.Commands["rm \"/data/local/tmp/gapps.txt\""] = string.Empty; - adbClient.Commands["rm \"/data/local/tmp/logcat.bin\""] = string.Empty; + adbClient.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + adbClient.Commands["shell:pm install-create"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-create -p com.google.android.gms"] = "Success: created install session [936013062]"; + adbClient.Commands["shell:pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split0.apk \"/data/local/tmp/gapps.txt\""] = "Success"; + adbClient.Commands["shell:pm install-write 936013062 split1.apk \"/data/local/tmp/logcat.bin\""] = "Success"; + adbClient.Commands["shell:pm install-commit 936013062"] = "Success"; + adbClient.Commands["shell:rm \"/data/local/tmp/test.txt\""] = string.Empty; + adbClient.Commands["shell:rm \"/data/local/tmp/gapps.txt\""] = string.Empty; + adbClient.Commands["shell:rm \"/data/local/tmp/logcat.bin\""] = string.Empty; DeviceData device = new() { State = DeviceState.Online }; - PackageManager manager = new(adbClient, device); - manager.InstallMultiplePackage("Assets/test.txt", new string[] { "Assets/gapps.txt", "Assets/logcat.bin" }, false); + PackageManager manager = new(adbClient, device, (c, d) => syncService); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.Uploading, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing, + PackageInstallProgressState.PostInstall, + PackageInstallProgressState.Finished)) + { + manager.InstallMultiplePackage("Assets/test.txt", ["Assets/gapps.txt", "Assets/logcat.bin"]); + } + Assert.Equal(9, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create", adbClient.ReceivedCommands[1]); - Assert.Equal("pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[3]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[4]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[5]); - Assert.Equal("rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[6]); - Assert.Equal("rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[7]); - Assert.Equal("rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[8]); + Assert.Equal("shell:pm install-create", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 base.apk \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install-write 936013062 split0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[3]); + Assert.Equal("shell:pm install-write 936013062 split1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[4]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[5]); + Assert.Equal("shell:rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[6]); + Assert.Equal("shell:rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[7]); + Assert.Equal("shell:rm \"/data/local/tmp/test.txt\"", adbClient.ReceivedCommands[8]); Assert.Equal(3, syncService.UploadedFiles.Count); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/test.txt")); @@ -190,20 +249,31 @@ public void InstallMultiplePackageTest() Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/logcat.bin")); syncService.UploadedFiles.Clear(); - manager.InstallMultiplePackage(new string[] { "Assets/gapps.txt", "Assets/logcat.bin" }, "com.google.android.gms", false); - Assert.Equal(15, adbClient.ReceivedCommands.Count); - Assert.Equal("pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[9]); - Assert.Equal("pm install-write 936013062 splitapp0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[10]); - Assert.Equal("pm install-write 936013062 splitapp1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[11]); - Assert.Equal("pm install-commit 936013062", adbClient.ReceivedCommands[12]); - Assert.Equal("rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[6]); - Assert.Equal("rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[7]); + adbClient.ReceivedCommands.Clear(); + + using (EventTestHost eventTestHost = new( + manager, + PackageInstallProgressState.Uploading, + PackageInstallProgressState.CreateSession, + PackageInstallProgressState.WriteSession, + PackageInstallProgressState.Installing, + PackageInstallProgressState.PostInstall, + PackageInstallProgressState.Finished)) + { + manager.InstallMultiplePackage(["Assets/gapps.txt", "Assets/logcat.bin"], "com.google.android.gms"); + } + + Assert.Equal(6, adbClient.ReceivedCommands.Count); + Assert.Equal("shell:pm install-create -p com.google.android.gms", adbClient.ReceivedCommands[0]); + Assert.Equal("shell:pm install-write 936013062 split0.apk \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[1]); + Assert.Equal("shell:pm install-write 936013062 split1.apk \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[2]); + Assert.Equal("shell:pm install-commit 936013062", adbClient.ReceivedCommands[3]); + Assert.Equal("shell:rm \"/data/local/tmp/gapps.txt\"", adbClient.ReceivedCommands[4]); + Assert.Equal("shell:rm \"/data/local/tmp/logcat.bin\"", adbClient.ReceivedCommands[5]); Assert.Equal(2, syncService.UploadedFiles.Count); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/gapps.txt")); Assert.True(syncService.UploadedFiles.ContainsKey("/data/local/tmp/logcat.bin")); - - Factories.Reset(); } [Fact] @@ -215,8 +285,8 @@ public void UninstallPackageTest() }; DummyAdbClient client = new(); - client.Commands["pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; - client.Commands["pm uninstall com.android.gallery3d"] = "Success"; + client.Commands["shell:pm list packages -f"] = "package:/system/app/Gallery2/Gallery2.apk=com.android.gallery3d"; + client.Commands["shell:pm uninstall com.android.gallery3d"] = "Success"; PackageManager manager = new(client, device); // Command should execute correctly; if the wrong command is passed an exception @@ -233,12 +303,75 @@ public void GetPackageVersionInfoTest() }; DummyAdbClient client = new(); - client.Commands["dumpsys package com.google.android.gms"] = File.ReadAllText("Assets/gapps.txt"); + client.Commands["shell:dumpsys package com.google.android.gms"] = File.ReadAllText("Assets/gapps.txt"); PackageManager manager = new(client, device, skipInit: true); VersionInfo versionInfo = manager.GetVersionInfo("com.google.android.gms"); Assert.Equal(11062448, versionInfo.VersionCode); Assert.Equal("11.0.62 (448-160311229)", versionInfo.VersionName); } + + private class EventTestHost : IDisposable + { + private readonly PackageManager manager; + private readonly PackageInstallProgressState[] states; + + private PackageInstallProgressState? state; + private int packageFinished; + private int packageRequired; + private double uploadProgress; + + private int step = 0; + + public EventTestHost(PackageManager manager, params PackageInstallProgressState[] states) + { + this.states = states; + this.manager = manager; + manager.InstallProgressChanged += OnInstallProgressChanged; + } + + public void OnInstallProgressChanged(object sender, InstallProgressEventArgs args) + { + if (args.State == state) + { + Assert.True(uploadProgress <= args.UploadProgress, $"{nameof(args.UploadProgress)}: {args.UploadProgress} is less than {uploadProgress}."); + Assert.True(packageFinished <= args.PackageFinished, $"{nameof(args.PackageFinished)}: {args.PackageFinished} is less than {packageFinished}."); + } + else + { + Assert.Equal(states[step++], args.State); + } + + if (args.State is + PackageInstallProgressState.CreateSession + or PackageInstallProgressState.Installing + or PackageInstallProgressState.Finished) + { + Assert.Equal(0, args.UploadProgress); + Assert.Equal(0, args.PackageRequired); + Assert.Equal(0, args.PackageFinished); + } + else + { + if (packageRequired == 0) + { + packageRequired = args.PackageRequired; + } + else + { + Assert.Equal(packageRequired, args.PackageRequired); + } + } + + state = args.State; + packageFinished = args.PackageFinished; + uploadProgress = args.UploadProgress; + } + + public void Dispose() + { + manager.InstallProgressChanged -= OnInstallProgressChanged; + } + } } } diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/EnvironmentVariablesReceiverTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/EnvironmentVariablesReceiverTests.cs index f37383d1..88bd4b29 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/EnvironmentVariablesReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/EnvironmentVariablesReceiverTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands.Tests { /// /// Tests the class. diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/GetPropReceiverTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/GetPropReceiverTests.cs index ec9921e4..da7f71b9 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/GetPropReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/GetPropReceiverTests.cs @@ -1,8 +1,7 @@ -using AdvancedSharpAdbClient.Tests; -using System.Collections.Generic; +using System.Collections.Generic; using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands.Tests { public class GetPropReceiverTests { @@ -15,7 +14,7 @@ public void ListPropertiesTest() }; DummyAdbClient client = new(); - client.Commands["/system/bin/getprop"] = @"[init.svc.BGW]: [running] + client.Commands["shell:/system/bin/getprop"] = @"[init.svc.BGW]: [running] [init.svc.MtkCodecService]: [running] [init.svc.bootanim]: [stopped]"; diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/InstallOutputReceiverTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/InstallOutputReceiverTests.cs index e615b048..a0871234 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/InstallOutputReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/InstallOutputReceiverTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands.Tests { public class InstallOutputReceiverTests { diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/PackageManagerReceiverTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/PackageManagerReceiverTests.cs index cf944bc2..1e2086fa 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/PackageManagerReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/PackageManagerReceiverTests.cs @@ -1,7 +1,6 @@ -using AdvancedSharpAdbClient.Tests; -using Xunit; +using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands.Tests { public class PackageManagerReceiverTests { @@ -16,8 +15,8 @@ public void ParseThirdPartyPackage() DummyAdbClient client = new(); - PackageManager manager = new(client, device, thirdPartyOnly: false, syncServiceFactory: null, skipInit: true); - PackageManagerReceiver receiver = new(device, manager); + PackageManager manager = new(client, device, syncServiceFactory: null, skipInit: true); + PackageManagerReceiver receiver = new(manager); // Act receiver.AddOutput("package:/data/app/com.google.android.apps.plus-qQaDuXCpNqJuQSbIS6OxGA==/base.apk=com.google.android.apps.plus"); diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/ProcessOutputReceiverTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/ProcessOutputReceiverTests.cs index 78c82b4c..80fb796d 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/ProcessOutputReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/ProcessOutputReceiverTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands.Tests { /// /// Tests the class. diff --git a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/VersionInfoReceiverTests.cs b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/VersionInfoReceiverTests.cs index 630ee6f4..879856d1 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/VersionInfoReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceCommands/Receivers/VersionInfoReceiverTests.cs @@ -8,7 +8,7 @@ using System.IO; using Xunit; -namespace AdvancedSharpAdbClient.DeviceCommands.Tests +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands.Tests { /// /// Tests the class. @@ -31,11 +31,11 @@ public void GetVersionTest() Assert.Null(receiver.GetVersionCode(string.Empty)); Assert.Null(receiver.GetVersionCode(" versionCode=10210targetSdk=18")); - Assert.Equal("4.7.1", (string)receiver.GetVersionName(" versionName=4.7.1")); - Assert.Null((string)receiver.GetVersionName(null)); - Assert.Null((string)receiver.GetVersionName(" test")); - Assert.Null((string)receiver.GetVersionName(" versionName")); - Assert.Equal(string.Empty, (string)receiver.GetVersionName(" versionName=")); + Assert.Equal("4.7.1", receiver.GetVersionName(" versionName=4.7.1")); + Assert.Null(receiver.GetVersionName(null)); + Assert.Null(receiver.GetVersionName(" test")); + Assert.Null(receiver.GetVersionName(" versionName")); + Assert.Equal(string.Empty, receiver.GetVersionName(" versionName=")); string dumpsys = string.Join(Environment.NewLine, File.ReadAllLines(@"Assets/dumpsys_package.txt")); receiver = new VersionInfoReceiver(); diff --git a/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.Async.cs b/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.Async.cs index 0727d0ad..bd455b16 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.Async.cs @@ -1,274 +1,257 @@ using System.Linq; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace AdvancedSharpAdbClient.Tests { public partial class DeviceMonitorTests { - //[Fact] - //public async void DeviceDisconnectedAsyncTest() - //{ - // Socket.WaitForNewData = true; - - // await using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // await RunTestAsync( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests("host:track-devices"), - // async () => - // { - // await monitor.StartAsync(); - - // Assert.Single(monitor.Devices); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // // Device disconnects - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages(""), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Empty(monitor.Devices); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Single(sink.DisconnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.DisconnectedEvents[0].Device.Serial); - // }); - //} - - //[Fact] - //public async void DeviceConnectedAsyncTest() - //{ - // Socket.WaitForNewData = true; - - // await using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // await RunTestAsync( - // OkResponse, - // ResponseMessages(""), - // Requests("host:track-devices"), - // async () => - // { - // await monitor.StartAsync(); - - // Assert.Empty(monitor.Devices); - // Assert.Empty(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Empty(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // // Device disconnects - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Single(monitor.Devices); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); - // }); - //} - - //[Fact] - //public async void StartInitialDeviceListAsyncTest() - //{ - // Socket.WaitForNewData = true; - - // await using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // await RunTestAsync( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests("host:track-devices"), - // async () => - // { - // await monitor.StartAsync(); - - // Assert.Single(monitor.Devices); - // Assert.Equal("169.254.109.177:5555", monitor.Devices.ElementAt(0).Serial); - // Assert.Single(sink.ConnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - //} - - //[Fact] - //public async void DeviceChanged_TriggeredWhenStatusChangedAsyncTest() - //{ - // Socket.WaitForNewData = true; - - // await using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // await RunTestAsync( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\toffline\n"), - // Requests("host:track-devices"), - // async () => - // { - // await monitor.StartAsync(); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // sink.ResetSignals(); - - // // Device disconnects - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Online, monitor.Devices.ElementAt(0).State); - // Assert.Empty(sink.ConnectedEvents); - // Assert.Single(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.ChangedEvents[0].Device.Serial); - // }); - //} - - //[Fact] - //public async void DeviceChanged_NoTriggerIfStatusIsSameAsyncTest() - //{ - // Socket.WaitForNewData = true; - - // await using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // await RunTestAsync( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\toffline\n"), - // Requests("host:track-devices"), - // async () => - // { - // await monitor.StartAsync(); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // sink.ResetSignals(); - - // // Something happens but device does not change - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages("169.254.109.177:5555\toffline\n"), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); - // Assert.Empty(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - //} - - ///// - ///// Tests the in a case where the adb server dies in the middle of the monitor - ///// loop. The should detect this condition and restart the adb server. - ///// - //[Fact] - //public async void AdbKilledAsyncTest() - //{ - // DummyAdbServer dummyAdbServer = new(); - // AdbServer.Instance = dummyAdbServer; - - // Socket.WaitForNewData = true; - - // await using DeviceMonitor monitor = new(Socket); - // await RunTestAsync( - // new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, - // ResponseMessages( - // DummyAdbSocket.ServerDisconnected, - // string.Empty), - // Requests( - // "host:track-devices", - // "host:track-devices"), - // async () => - // { - // await monitor.StartAsync(); - - // Assert.True(Socket.DidReconnect); - // Assert.True(dummyAdbServer.WasRestarted); - // }); - //} + [Fact] + public async void DeviceDisconnectedAsyncTest() + { + Socket.WaitForNewData = true; + + await using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + await RunTestAsync( + OkResponse, + ["169.254.109.177:5555\tdevice\n"], + ["host:track-devices"], + () => monitor.StartAsync()); + + Assert.Single(monitor.Devices); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Device disconnects + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = await RunTestAsync( + NoResponses, + [string.Empty], + NoRequests, + () => Task.Run(() => eventWaiter.WaitOne(1000))); + + Assert.Empty(monitor.Devices); + Assert.Empty(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Single(sink.DisconnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.DisconnectedEvents[0].Device.Serial); + } + + [Fact] + public async void DeviceConnectedAsyncTest() + { + Socket.WaitForNewData = true; + + await using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + await RunTestAsync( + OkResponse, + [string.Empty], + ["host:track-devices"], + () => monitor.StartAsync()); + + Assert.Empty(monitor.Devices); + Assert.Empty(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Empty(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Device disconnects + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = await RunTestAsync( + NoResponses, + ["169.254.109.177:5555\tdevice\n"], + NoRequests, + () => Task.Run(() => eventWaiter.WaitOne(1000))); + + Assert.Single(monitor.Devices); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); + } + + /// + /// Tests the method. + /// + [Fact] + public async void StartInitialDeviceListAsyncTest() + { + Socket.WaitForNewData = true; + + await using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + await RunTestAsync( + OkResponse, + ["169.254.109.177:5555\tdevice\n"], + ["host:track-devices"], + () => monitor.StartAsync()); + + Assert.Single(monitor.Devices); + Assert.Equal("169.254.109.177:5555", monitor.Devices[0].Serial); + Assert.Single(sink.ConnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + } + + [Fact] + public async void TriggeredWhenStatusChangedAsyncTest() + { + Socket.WaitForNewData = true; + + await using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + await RunTestAsync( + OkResponse, + ["169.254.109.177:5555\toffline\n"], + ["host:track-devices"], + () => monitor.StartAsync()); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Offline, monitor.Devices[0].State); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Device disconnects + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = await RunTestAsync( + NoResponses, + ["169.254.109.177:5555\tdevice\n"], + NoRequests, + () => Task.Run(() => eventWaiter.WaitOne(1000))); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Online, monitor.Devices[0].State); + Assert.Empty(sink.ConnectedEvents); + Assert.Single(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.ChangedEvents[0].Device.Serial); + } + + [Fact] + public async void NoTriggerIfStatusIsSameAsyncTest() + { + Socket.WaitForNewData = true; + + await using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + await RunTestAsync( + OkResponse, + ["169.254.109.177:5555\toffline\n"], + ["host:track-devices"], + () => monitor.StartAsync()); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Something happens but device does not change + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = await RunTestAsync( + NoResponses, + ["169.254.109.177:5555\toffline\n"], + NoRequests, + () => Task.Run(() => eventWaiter.WaitOne(1000))); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); + Assert.Empty(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Empty(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + } + + /// + /// Tests the in a case where the adb server dies in the middle of the monitor + /// loop. The should detect this condition and restart the adb server. + /// + [Fact] + public async void AdbKilledAsyncTest() + { + DummyAdbServer dummyAdbServer = new(); + AdbServer.Instance = dummyAdbServer; + + Socket.WaitForNewData = true; + + await using DeviceMonitor monitor = new(Socket); + await RunTestAsync( + OkResponses(2), + [DummyAdbSocket.ServerDisconnected, string.Empty], + ["host:track-devices", "host:track-devices"], + () => monitor.StartAsync()); + + Assert.True(Socket.DidReconnect); + Assert.True(dummyAdbServer.WasRestarted); + } } } diff --git a/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.cs b/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.cs index c1a78c5f..42ec5a56 100644 --- a/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.cs +++ b/AdvancedSharpAdbClient.Tests/DeviceMonitorTests.cs @@ -18,282 +18,270 @@ public DeviceMonitorTests() : base(integrationTest: false, doDispose: true) { } - //[Fact] - //public void ConstructorTest() - //{ - // using DeviceMonitor monitor = new(Socket); - // Assert.NotNull(monitor.Devices); - // Assert.Empty(monitor.Devices); - // Assert.Equal(Socket, monitor.Socket); - // Assert.False(monitor.IsRunning); - //} - - //[Fact] - //public void ConstructorNullTest() => _ = Assert.Throws(() => new DeviceMonitor(null)); - - //[Fact] - //public void DeviceDisconnectedTest() - //{ - // Socket.WaitForNewData = true; - - // using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // RunTest( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests("host:track-devices"), - // () => - // { - // monitor.Start(); - - // Assert.Single(monitor.Devices); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // // Device disconnects - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages(""), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Empty(monitor.Devices); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Single(sink.DisconnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.DisconnectedEvents[0].Device.Serial); - // }); - //} - - //[Fact] - //public void DeviceConnectedTest() - //{ - // Socket.WaitForNewData = true; - - // using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // RunTest( - // OkResponse, - // ResponseMessages(""), - // Requests("host:track-devices"), - // () => - // { - // monitor.Start(); - - // Assert.Empty(monitor.Devices); - // Assert.Empty(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Empty(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // // Device disconnects - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Single(monitor.Devices); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); - // }); - //} - - //[Fact] - //public void StartInitialDeviceListTest() - //{ - // Socket.WaitForNewData = true; - - // using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // RunTest( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests("host:track-devices"), - // () => - // { - // monitor.Start(); - - // Assert.Single(monitor.Devices); - // Assert.Equal("169.254.109.177:5555", monitor.Devices.ElementAt(0).Serial); - // Assert.Single(sink.ConnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - //} - - //[Fact] - //public void DeviceChanged_TriggeredWhenStatusChangedTest() - //{ - // Socket.WaitForNewData = true; - - // using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // RunTest( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\toffline\n"), - // Requests("host:track-devices"), - // () => - // { - // monitor.Start(); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // sink.ResetSignals(); - - // // Device disconnects - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages("169.254.109.177:5555\tdevice\n"), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Online, monitor.Devices.ElementAt(0).State); - // Assert.Empty(sink.ConnectedEvents); - // Assert.Single(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // Assert.Equal("169.254.109.177:5555", sink.ChangedEvents[0].Device.Serial); - // }); - //} - - //[Fact] - //public void DeviceChanged_NoTriggerIfStatusIsSameTest() - //{ - // Socket.WaitForNewData = true; - - // using DeviceMonitor monitor = new(Socket); - // DeviceMonitorSink sink = new(monitor); - - // Assert.Empty(monitor.Devices); - - // // Start the monitor, detect the initial device. - // RunTest( - // OkResponse, - // ResponseMessages("169.254.109.177:5555\toffline\n"), - // Requests("host:track-devices"), - // () => - // { - // monitor.Start(); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); - // Assert.Single(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - - // Socket.ResponseMessages.Clear(); - // Socket.Responses.Clear(); - // Socket.Requests.Clear(); - - // sink.ResetSignals(); - - // // Something happens but device does not change - // ManualResetEvent eventWaiter = sink.CreateEventSignal(); - - // RunTest( - // NoResponses, - // ResponseMessages("169.254.109.177:5555\toffline\n"), - // Requests(), - // () => - // { - // eventWaiter.WaitOne(1000); - - // Assert.Single(monitor.Devices); - // Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); - // Assert.Empty(sink.ConnectedEvents); - // Assert.Empty(sink.ChangedEvents); - // Assert.Single(sink.NotifiedEvents); - // Assert.Empty(sink.DisconnectedEvents); - // }); - //} - - ///// - ///// Tests the in a case where the adb server dies in the middle of the monitor - ///// loop. The should detect this condition and restart the adb server. - ///// - //[Fact] - //public void AdbKilledTest() - //{ - // DummyAdbServer dummyAdbServer = new(); - // AdbServer.Instance = dummyAdbServer; - - // Socket.WaitForNewData = true; - - // using DeviceMonitor monitor = new(Socket); - // RunTest( - // new AdbResponse[] { AdbResponse.OK, AdbResponse.OK }, - // ResponseMessages( - // DummyAdbSocket.ServerDisconnected, - // string.Empty), - // Requests( - // "host:track-devices", - // "host:track-devices"), - // () => - // { - // monitor.Start(); - - // Assert.True(Socket.DidReconnect); - // Assert.True(dummyAdbServer.WasRestarted); - // }); - //} + /// + /// Tests the method. + /// + [Fact] + public void ConstructorTest() + { + using DeviceMonitor monitor = new(Socket); + Assert.NotNull(monitor.Devices); + Assert.Empty(monitor.Devices); + Assert.Equal(Socket, monitor.Socket); + Assert.False(monitor.IsRunning); + } + + /// + /// Tests the method. + /// + [Fact] + public void ConstructorNullTest() => _ = Assert.Throws(() => new DeviceMonitor((IAdbSocket)null)); + + [Fact] + public void DeviceDisconnectedTest() + { + Socket.WaitForNewData = true; + + using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + RunTest( + OkResponse, + ["169.254.109.177:5555\tdevice\n"], + ["host:track-devices"], + monitor.Start); + + Assert.Single(monitor.Devices); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + sink.ResetSignals(); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + // Device disconnects + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = RunTest( + NoResponses, + [string.Empty], + NoRequests, + () => eventWaiter.WaitOne(1000)); + + Assert.Empty(monitor.Devices); + Assert.Empty(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Single(sink.DisconnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.DisconnectedEvents[0].Device.Serial); + } + + [Fact] + public void DeviceConnectedTest() + { + Socket.WaitForNewData = true; + + using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + RunTest( + OkResponse, + [string.Empty], + ["host:track-devices"], + monitor.Start); + + Assert.Empty(monitor.Devices); + Assert.Empty(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Empty(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Device disconnects + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = RunTest( + NoResponses, + ["169.254.109.177:5555\tdevice\n"], + NoRequests, + () => eventWaiter.WaitOne(1000)); + + Assert.Single(monitor.Devices); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); + } + + /// + /// Tests the method. + /// + [Fact] + public void StartInitialDeviceListTest() + { + Socket.WaitForNewData = true; + + using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + RunTest( + OkResponse, + ["169.254.109.177:5555\tdevice\n"], + ["host:track-devices"], + monitor.Start); + + Assert.Single(monitor.Devices); + Assert.Equal("169.254.109.177:5555", monitor.Devices[0].Serial); + Assert.Single(sink.ConnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.ConnectedEvents[0].Device.Serial); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + } + + [Fact] + public void TriggeredWhenStatusChangedTest() + { + Socket.WaitForNewData = true; + + using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + RunTest( + OkResponse, + ["169.254.109.177:5555\toffline\n"], + ["host:track-devices"], + monitor.Start); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Offline, monitor.Devices[0].State); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Device disconnects + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = RunTest( + NoResponses, + ["169.254.109.177:5555\tdevice\n"], + NoRequests, + () => eventWaiter.WaitOne(1000)); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Online, monitor.Devices[0].State); + Assert.Empty(sink.ConnectedEvents); + Assert.Single(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + Assert.Equal("169.254.109.177:5555", sink.ChangedEvents[0].Device.Serial); + } + + [Fact] + public void NoTriggerIfStatusIsSameTest() + { + Socket.WaitForNewData = true; + + using DeviceMonitor monitor = new(Socket); + DeviceMonitorSink sink = new(monitor); + + Assert.Empty(monitor.Devices); + + // Start the monitor, detect the initial device. + RunTest( + OkResponse, + ["169.254.109.177:5555\toffline\n"], + ["host:track-devices"], + monitor.Start); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); + Assert.Single(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Single(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + + Socket.ResponseMessages.Clear(); + Socket.Responses.Clear(); + Socket.Requests.Clear(); + + sink.ResetSignals(); + + // Something happens but device does not change + ManualResetEvent eventWaiter = sink.CreateEventSignal(); + + _ = RunTest( + NoResponses, + ["169.254.109.177:5555\toffline\n"], + NoRequests, + () => eventWaiter.WaitOne(1000)); + + Assert.Single(monitor.Devices); + Assert.Equal(DeviceState.Offline, monitor.Devices.ElementAt(0).State); + Assert.Empty(sink.ConnectedEvents); + Assert.Empty(sink.ChangedEvents); + Assert.Single(sink.NotifiedEvents); + Assert.Empty(sink.ListChangedEvents); + Assert.Empty(sink.DisconnectedEvents); + } + + /// + /// Tests the in a case where the adb server dies in the middle of the monitor + /// loop. The should detect this condition and restart the adb server. + /// + [Fact] + public void AdbKilledTest() + { + DummyAdbServer dummyAdbServer = new(); + AdbServer.Instance = dummyAdbServer; + + Socket.WaitForNewData = true; + + using DeviceMonitor monitor = new(Socket); + RunTest( + OkResponses(2), + [DummyAdbSocket.ServerDisconnected, string.Empty], + ["host:track-devices", "host:track-devices"], + monitor.Start); + + Assert.True(Socket.DidReconnect); + Assert.True(dummyAdbServer.WasRestarted); + } } } diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DeviceMonitorSink.cs b/AdvancedSharpAdbClient.Tests/Dummys/DeviceMonitorSink.cs index 884e7e3c..ff039f1c 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DeviceMonitorSink.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DeviceMonitorSink.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Threading; namespace AdvancedSharpAdbClient.Tests @@ -12,12 +12,14 @@ public DeviceMonitorSink(DeviceMonitor monitor) Monitor.DeviceChanged += OnDeviceChanged; Monitor.DeviceNotified += OnDeviceNotified; Monitor.DeviceConnected += OnDeviceConnected; + Monitor.DeviceListChanged += OnDeviceListChanged; Monitor.DeviceDisconnected += OnDeviceDisconnected; - ChangedEvents = new Collection(); - NotifiedEvents = new Collection(); - ConnectedEvents = new Collection(); - DisconnectedEvents = new Collection(); + ChangedEvents = []; + NotifiedEvents = []; + ConnectedEvents = []; + ListChangedEvents = []; + DisconnectedEvents = []; } public void ResetSignals() @@ -25,29 +27,33 @@ public void ResetSignals() ChangedEvents.Clear(); NotifiedEvents.Clear(); ConnectedEvents.Clear(); + ListChangedEvents.Clear(); DisconnectedEvents.Clear(); } - public Collection DisconnectedEvents { get; private set; } + public List DisconnectedEvents { get; init; } - public Collection ConnectedEvents { get; private set; } + public List ListChangedEvents { get; init; } - public Collection NotifiedEvents { get; private set; } + public List ConnectedEvents { get; init; } - public Collection ChangedEvents { get; private set; } + public List NotifiedEvents { get; init; } - public DeviceMonitor Monitor { get; private set; } + public List ChangedEvents { get; init; } + + public DeviceMonitor Monitor { get; init; } public ManualResetEvent CreateEventSignal() { ManualResetEvent signal = new(false); Monitor.DeviceNotified += (sender, e) => signal.Set(); - Monitor.DeviceDisconnected += (sender, e) => signal.Set(); return signal; } protected virtual void OnDeviceDisconnected(object sender, DeviceDataConnectEventArgs e) => DisconnectedEvents.Add(e); + protected virtual void OnDeviceListChanged(object sender, DeviceDataNotifyEventArgs e) => ListChangedEvents.Add(e); + protected virtual void OnDeviceConnected(object sender, DeviceDataConnectEventArgs e) => ConnectedEvents.Add(e); protected virtual void OnDeviceNotified(object sender, DeviceDataNotifyEventArgs e) => NotifiedEvents.Add(e); diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbClient.cs b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbClient.cs index 0feec6a8..bd6717d8 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbClient.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbClient.cs @@ -1,7 +1,6 @@ -using AdvancedSharpAdbClient.Logs; -using System; +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Drawing; using System.IO; using System.Net; using System.Text; @@ -11,26 +10,62 @@ namespace AdvancedSharpAdbClient.Tests { + /// + /// A mock implementation of the class. + /// internal class DummyAdbClient : IAdbClient { - public Dictionary Commands { get; private set; } = new Dictionary(); + public Dictionary Commands { get; } = []; - public Collection ReceivedCommands { get; private set; } = new Collection(); + public List ReceivedCommands { get; } = []; - public EndPoint EndPoint { get; private set; } + public EndPoint EndPoint { get; init; } - public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver) => - ExecuteRemoteCommand(command, device, receiver, Encoding.Default); + public void ExecuteRemoteCommand(string command, DeviceData device, Encoding encoding) => + ExecuteServerCommand("shell", command, encoding); - public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding) + public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding) => + ExecuteServerCommand("shell", command, receiver, encoding); + + public Task ExecuteRemoteCommandAsync(string command, DeviceData device, Encoding encoding, CancellationToken cancellationToken = default) => + ExecuteServerCommandAsync("shell", command, encoding, cancellationToken); + + public Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) => + ExecuteServerCommandAsync("shell", command, receiver, encoding, cancellationToken); + + public void ExecuteServerCommand(string target, string command, Encoding encoding) + { + StringBuilder requestBuilder = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = requestBuilder.AppendFormat("{0}:", target); + } + _ = requestBuilder.Append(command); + + string request = requestBuilder.ToString(); + ReceivedCommands.Add(request); + } + + public void ExecuteServerCommand(string target, string command, IAdbSocket socket, Encoding encoding) => + ExecuteServerCommand(target, command, encoding); + + public void ExecuteServerCommand(string target, string command, IShellOutputReceiver receiver, Encoding encoding) { - ReceivedCommands.Add(command); + StringBuilder requestBuilder = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = requestBuilder.AppendFormat("{0}:", target); + } + _ = requestBuilder.Append(command); - if (Commands.ContainsKey(command)) + string request = requestBuilder.ToString(); + ReceivedCommands.Add(request); + + if (Commands.TryGetValue(request, out string value)) { if (receiver != null) { - StringReader reader = new(Commands[command]); + StringReader reader = new(value); while (reader.Peek() != -1) { @@ -42,26 +77,52 @@ public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutput } else { - throw new ArgumentOutOfRangeException(nameof(command), $"The command '{command}' was unexpected"); + throw new ArgumentOutOfRangeException(nameof(command), $"The command '{request}' was unexpected"); + } + } + + public void ExecuteServerCommand(string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, Encoding encoding) => + ExecuteServerCommand(target, command, receiver, encoding); + + public async Task ExecuteServerCommandAsync(string target, string command, Encoding encoding, CancellationToken cancellationToken = default) + { + await Task.Yield(); + + StringBuilder requestBuilder = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = requestBuilder.AppendFormat("{0}:", target); } + _ = requestBuilder.Append(command); + + string request = requestBuilder.ToString(); + ReceivedCommands.Add(request); } - public Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, CancellationToken cancellationToken = default) => - ExecuteRemoteCommandAsync(command, device, receiver, Encoding.Default, cancellationToken); + public Task ExecuteServerCommandAsync(string target, string command, IAdbSocket socket, Encoding encoding, CancellationToken cancellationToken) => + ExecuteServerCommandAsync(target, command, encoding, cancellationToken); - public Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) + public async Task ExecuteServerCommandAsync(string target, string command, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) { - ReceivedCommands.Add(command); + StringBuilder requestBuilder = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = requestBuilder.AppendFormat("{0}:", target); + } + _ = requestBuilder.Append(command); - if (Commands.ContainsKey(command)) + string request = requestBuilder.ToString(); + ReceivedCommands.Add(request); + + if (Commands.TryGetValue(request, out string value)) { if (receiver != null) { - StringReader reader = new(Commands[command]); + StringReader reader = new(value); while (reader.Peek() != -1) { - receiver.AddOutput(reader.ReadLine()); + receiver.AddOutput(await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)); } receiver.Flush(); @@ -69,207 +130,192 @@ public Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellO } else { - throw new ArgumentOutOfRangeException(nameof(command), $"The command '{command}' was unexpected"); + throw new ArgumentOutOfRangeException(nameof(command), $"The command '{request}' was unexpected"); } - - return Task.FromResult(true); } - #region Not Implemented - - public void BackBtn(DeviceData device) => throw new NotImplementedException(); - - public Task BackBtnAsync(DeviceData device) => throw new NotImplementedException(); - - public void ClearInput(DeviceData device, int charCount) => throw new NotImplementedException(); - - public Task ClearInputAsync(DeviceData device, int charCount, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public void Click(DeviceData device, Cords cords) => throw new NotImplementedException(); + public Task ExecuteServerCommandAsync(string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken) => + ExecuteServerCommandAsync(target, command, receiver, encoding, cancellationToken); - public void Click(DeviceData device, int x, int y) => throw new NotImplementedException(); - - public Task ClickAsync(DeviceData device, Cords cords, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ClickAsync(DeviceData device, int x, int y, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public string Connect(DnsEndPoint endpoint) => throw new NotImplementedException(); + #region Not Implemented - public Task ConnectAsync(DnsEndPoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.Click(DeviceData device, Point cords) => throw new NotImplementedException(); - public int CreateForward(DeviceData device, string local, string remote, bool allowRebind) => throw new NotImplementedException(); + void IAdbClient.Click(DeviceData device, int x, int y) => throw new NotImplementedException(); - public int CreateForward(DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind) => throw new NotImplementedException(); + Task IAdbClient.ClickAsync(DeviceData device, Point cords, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task CreateForwardAsync(DeviceData device, string local, string remote, bool allowRebind, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.ClickAsync(DeviceData device, int x, int y, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task CreateForwardAsync(DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind, CancellationToken cancellationToken) => throw new NotImplementedException(); + string IAdbClient.Connect(DnsEndPoint endpoint) => throw new NotImplementedException(); - public Framebuffer CreateRefreshableFramebuffer(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.ConnectAsync(DnsEndPoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); - public int CreateReverseForward(DeviceData device, string remote, string local, bool allowRebind) => throw new NotImplementedException(); + int IAdbClient.CreateForward(DeviceData device, string local, string remote, bool allowRebind) => throw new NotImplementedException(); - public Task CreateReverseForwardAsync(DeviceData device, string remote, string local, bool allowRebind, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.CreateForwardAsync(DeviceData device, string local, string remote, bool allowRebind, CancellationToken cancellationToken) => throw new NotImplementedException(); - public string Disconnect(DnsEndPoint endpoint) => throw new NotImplementedException(); + Framebuffer IAdbClient.CreateRefreshableFramebuffer(DeviceData device) => throw new NotImplementedException(); - public Task DisconnectAsync(DnsEndPoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); + int IAdbClient.CreateReverseForward(DeviceData device, string remote, string local, bool allowRebind) => throw new NotImplementedException(); - public XmlDocument DumpScreen(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.CreateReverseForwardAsync(DeviceData device, string remote, string local, bool allowRebind, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task DumpScreenAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + string IAdbClient.Disconnect(DnsEndPoint endpoint) => throw new NotImplementedException(); - public string DumpScreenString(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.DisconnectAsync(DnsEndPoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task DumpScreenStringAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + XmlDocument IAdbClient.DumpScreen(DeviceData device) => throw new NotImplementedException(); - public IAsyncEnumerable FindAsyncElements(DeviceData device, string xpath, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.DumpScreenAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Element FindElement(DeviceData device, string xpath, TimeSpan timeout = default) => throw new NotImplementedException(); + string IAdbClient.DumpScreenString(DeviceData device) => throw new NotImplementedException(); - public Task FindElementAsync(DeviceData device, string xpath, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.DumpScreenStringAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public IEnumerable FindElements(DeviceData device, string xpath, TimeSpan timeout = default) => throw new NotImplementedException(); +#if WINDOWS10_0_17763_0_OR_GREATER + Windows.Data.Xml.Dom.XmlDocument IAdbClient.DumpScreenWinRT(DeviceData device) => throw new NotImplementedException(); - public Task> FindElementsAsync(DeviceData device, string xpath, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.DumpScreenWinRTAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); +#endif - public int GetAdbVersion() => throw new NotImplementedException(); + IAsyncEnumerable IAdbClient.FindAsyncElements(DeviceData device, string xpath, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task GetAdbVersionAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + Element IAdbClient.FindElement(DeviceData device, string xpath, TimeSpan timeout) => throw new NotImplementedException(); - public AppStatus GetAppStatus(DeviceData device, string packageName) => throw new NotImplementedException(); + Task IAdbClient.FindElementAsync(DeviceData device, string xpath, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task GetAppStatusAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); + IEnumerable IAdbClient.FindElements(DeviceData device, string xpath, TimeSpan timeout) => throw new NotImplementedException(); - public IEnumerable GetDevices() => throw new NotImplementedException(); + Task> IAdbClient.FindElementsAsync(DeviceData device, string xpath, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task> GetDevicesAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + int IAdbClient.GetAdbVersion() => throw new NotImplementedException(); - public IEnumerable GetFeatureSet(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.GetAdbVersionAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task> GetFeatureSetAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + AppStatus IAdbClient.GetAppStatus(DeviceData device, string packageName) => throw new NotImplementedException(); - public Framebuffer GetFrameBuffer(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.GetAppStatusAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task GetFrameBufferAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + IEnumerable IAdbClient.GetDevices() => throw new NotImplementedException(); - public void HomeBtn(DeviceData device) => throw new NotImplementedException(); + Task> IAdbClient.GetDevicesAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task HomeBtnAsync(DeviceData device) => throw new NotImplementedException(); + IEnumerable IAdbClient.GetFeatureSet(DeviceData device) => throw new NotImplementedException(); - public void Install(DeviceData device, Stream apk, params string[] arguments) => throw new NotImplementedException(); + Task> IAdbClient.GetFeatureSetAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task InstallAsync(DeviceData device, Stream apk, params string[] arguments) => throw new NotImplementedException(); + Framebuffer IAdbClient.GetFrameBuffer(DeviceData device) => throw new NotImplementedException(); - public Task InstallAsync(DeviceData device, Stream apk, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); + Task IAdbClient.GetFrameBufferAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public void InstallCommit(DeviceData device, string session) => throw new NotImplementedException(); + void IAdbClient.Install(DeviceData device, Stream apk, params string[] arguments) => throw new NotImplementedException(); - public Task InstallCommitAsync(DeviceData device, string session, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.InstallAsync(DeviceData device, Stream apk, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); - public string InstallCreate(DeviceData device, string packageName = null, params string[] arguments) => throw new NotImplementedException(); + void IAdbClient.InstallCommit(DeviceData device, string session) => throw new NotImplementedException(); - public Task InstallCreateAsync(DeviceData device, string packageName, params string[] arguments) => throw new NotImplementedException(); + Task IAdbClient.InstallCommitAsync(DeviceData device, string session, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task InstallCreateAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); + string IAdbClient.InstallCreate(DeviceData device, string packageName, params string[] arguments) => throw new NotImplementedException(); - public void InstallMultiple(DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments) => throw new NotImplementedException(); + Task IAdbClient.InstallCreateAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); - public void InstallMultiple(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments) => throw new NotImplementedException(); + void IAdbClient.InstallMultiple(DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments) => throw new NotImplementedException(); - public Task InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments) => throw new NotImplementedException(); + void IAdbClient.InstallMultiple(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments) => throw new NotImplementedException(); - public Task InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); + Task IAdbClient.InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); - public Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments) => throw new NotImplementedException(); + Task IAdbClient.InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); - public Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); + void IAdbClient.InstallWrite(DeviceData device, Stream apk, string apkName, string session) => throw new NotImplementedException(); - public void InstallWrite(DeviceData device, Stream apk, string apkName, string session) => throw new NotImplementedException(); + Task IAdbClient.InstallWriteAsync(DeviceData device, Stream apk, string apkName, string session, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task InstallWriteAsync(DeviceData device, Stream apk, string apkName, string session, CancellationToken cancellationToken) => throw new NotImplementedException(); + bool IAdbClient.IsAppInForeground(DeviceData device, string packageName) => throw new NotImplementedException(); - public bool IsAppRunning(DeviceData device, string packageName) => throw new NotImplementedException(); + Task IAdbClient.IsAppInForegroundAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task IsAppRunningAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); + bool IAdbClient.IsAppRunning(DeviceData device, string packageName) => throw new NotImplementedException(); - public bool IsCurrentApp(DeviceData device, string packageName) => throw new NotImplementedException(); + Task IAdbClient.IsAppRunningAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task IsCurrentAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.KillAdb() => throw new NotImplementedException(); - public void KillAdb() => throw new NotImplementedException(); + Task IAdbClient.KillAdbAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task KillAdbAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + IEnumerable IAdbClient.ListForward(DeviceData device) => throw new NotImplementedException(); - public IEnumerable ListForward(DeviceData device) => throw new NotImplementedException(); + Task> IAdbClient.ListForwardAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task> ListForwardAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + IEnumerable IAdbClient.ListReverseForward(DeviceData device) => throw new NotImplementedException(); - public IEnumerable ListReverseForward(DeviceData device) => throw new NotImplementedException(); + Task> IAdbClient.ListReverseForwardAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task> ListReverseForwardAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + string IAdbClient.Pair(DnsEndPoint endpoint, string code) => throw new NotImplementedException(); - public string Pair(DnsEndPoint endpoint, string code) => throw new NotImplementedException(); + Task IAdbClient.PairAsync(DnsEndPoint endpoint, string code, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task PairAsync(DnsEndPoint endpoint, string code, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.Reboot(string into, DeviceData device) => throw new NotImplementedException(); - public void Reboot(string into, DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.RebootAsync(string into, DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task RebootAsync(string into, DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.RemoveAllForwards(DeviceData device) => throw new NotImplementedException(); - public void RemoveAllForwards(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.RemoveAllForwardsAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task RemoveAllForwardsAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.RemoveAllReverseForwards(DeviceData device) => throw new NotImplementedException(); - public void RemoveAllReverseForwards(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.RemoveAllReverseForwardsAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task RemoveAllReverseForwardsAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.RemoveForward(DeviceData device, int localPort) => throw new NotImplementedException(); - public void RemoveForward(DeviceData device, int localPort) => throw new NotImplementedException(); + Task IAdbClient.RemoveForwardAsync(DeviceData device, int localPort, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task RemoveForwardAsync(DeviceData device, int localPort, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.RemoveReverseForward(DeviceData device, string remote) => throw new NotImplementedException(); - public void RemoveReverseForward(DeviceData device, string remote) => throw new NotImplementedException(); + Task IAdbClient.RemoveReverseForwardAsync(DeviceData device, string remote, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task RemoveReverseForwardAsync(DeviceData device, string remote, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.Root(DeviceData device) => throw new NotImplementedException(); - public void Root(DeviceData device) => throw new NotImplementedException(); + Task IAdbClient.RootAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task RootAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.RunLogService(DeviceData device, Action messageSink, params LogId[] logNames) => throw new NotImplementedException(); - public void RunLogService(DeviceData device, Action messageSink, params LogId[] logNames) => throw new NotImplementedException(); + Task IAdbClient.RunLogServiceAsync(DeviceData device, Action messageSink, CancellationToken cancellationToken, params LogId[] logNames) => throw new NotImplementedException(); - public Task RunLogServiceAsync(DeviceData device, Action messageSink, params LogId[] logNames) => throw new NotImplementedException(); + void IAdbClient.SendKeyEvent(DeviceData device, string key) => throw new NotImplementedException(); - public Task RunLogServiceAsync(DeviceData device, Action messageSink, CancellationToken cancellationToken, params LogId[] logNames) => throw new NotImplementedException(); + Task IAdbClient.SendKeyEventAsync(DeviceData device, string key, CancellationToken cancellationToken) => throw new NotImplementedException(); - public void SendKeyEvent(DeviceData device, string key) => throw new NotImplementedException(); + void IAdbClient.SendText(DeviceData device, string text) => throw new NotImplementedException(); - public Task SendKeyEventAsync(DeviceData device, string key, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.SendTextAsync(DeviceData device, string text, CancellationToken cancellationToken) => throw new NotImplementedException(); - public void SendText(DeviceData device, string text) => throw new NotImplementedException(); + void IAdbClient.StartApp(DeviceData device, string packageName) => throw new NotImplementedException(); - public Task SendTextAsync(DeviceData device, string text, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.StartAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); - public void StartApp(DeviceData device, string packageName) => throw new NotImplementedException(); + void IAdbClient.StopApp(DeviceData device, string packageName) => throw new NotImplementedException(); - public Task StartAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.StopAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); - public void StopApp(DeviceData device, string packageName) => throw new NotImplementedException(); + void IAdbClient.Swipe(DeviceData device, Element first, Element second, long speed) => throw new NotImplementedException(); - public Task StopAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.Swipe(DeviceData device, int x1, int y1, int x2, int y2, long speed) => throw new NotImplementedException(); - public void Swipe(DeviceData device, Element first, Element second, long speed) => throw new NotImplementedException(); + Task IAdbClient.SwipeAsync(DeviceData device, Element first, Element second, long speed, CancellationToken cancellationToken) => throw new NotImplementedException(); - public void Swipe(DeviceData device, int x1, int y1, int x2, int y2, long speed) => throw new NotImplementedException(); + Task IAdbClient.SwipeAsync(DeviceData device, int x1, int y1, int x2, int y2, long speed, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task SwipeAsync(DeviceData device, Element first, Element second, long speed, CancellationToken cancellationToken) => throw new NotImplementedException(); + void IAdbClient.Uninstall(DeviceData device, string packageName, params string[] arguments) => throw new NotImplementedException(); - public Task SwipeAsync(DeviceData device, int x1, int y1, int x2, int y2, long speed, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.UninstallAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments) => throw new NotImplementedException(); - public void Unroot(DeviceData device) => throw new NotImplementedException(); + void IAdbClient.Unroot(DeviceData device) => throw new NotImplementedException(); - public Task UnrootAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); + Task IAdbClient.UnrootAsync(DeviceData device, CancellationToken cancellationToken) => throw new NotImplementedException(); #endregion } diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbCommandLineClient.cs b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbCommandLineClient.cs index ff2b269f..7987d578 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbCommandLineClient.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbCommandLineClient.cs @@ -7,7 +7,7 @@ namespace AdvancedSharpAdbClient.Tests { /// - /// + /// A mock implementation of the class. /// internal class DummyAdbCommandLineClient : AdbCommandLineClient { @@ -19,41 +19,41 @@ public DummyAdbCommandLineClient() : base(ServerName) public bool ServerStarted { get; private set; } - public override bool IsValidAdbFile(string adbPath) => - // No validation done in the dummy adb client. - true; + // No validation done in the dummy adb client. + public override bool CheckFileExists(string adbPath) => true; - protected override int RunAdbProcessInner(string command, List errorOutput, List standardOutput) + protected override int RunProcess(string filename, string command, ICollection errorOutput, ICollection standardOutput) { - errorOutput?.Add(null); + if (filename == AdbPath) + { + errorOutput?.Add(null); - standardOutput?.Add(null); + standardOutput?.Add(null); - if (command == "start-server") - { - ServerStarted = true; - } - else if (command == "version") - { - if (standardOutput != null && Version != null) + if (command == "start-server") { - standardOutput.Add($"Android Debug Bridge version {Version.ToString(3)}"); + ServerStarted = true; + } + else if (command == "version") + { + if (standardOutput != null && Version != null) + { + standardOutput.Add($"Android Debug Bridge version {Version.ToString(3)}"); + } + } + else + { + throw new ArgumentOutOfRangeException(nameof(command)); } - } - else - { - throw new ArgumentOutOfRangeException(nameof(command)); } return 0; } - protected override Task RunAdbProcessInnerAsync(string command, List errorOutput, List standardOutput, CancellationToken cancellationToken = default) + protected override async Task RunProcessAsync(string filename, string command, ICollection errorOutput, ICollection standardOutput, CancellationToken cancellationToken = default) { - int result = RunAdbProcessInner(command, errorOutput, standardOutput); - TaskCompletionSource tcs = new(); - tcs.SetResult(result); - return tcs.Task; + await Task.Yield(); + return RunProcess(filename, command, errorOutput, standardOutput); } private static string ServerName => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "adb.exe" : "adb"; diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbServer.cs b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbServer.cs index 3be2783a..95b5f5eb 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbServer.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbServer.cs @@ -31,15 +31,17 @@ internal class DummyAdbServer : IAdbServer public AdbServerStatus GetStatus() => Status; /// - public Task GetStatusAsync(CancellationToken cancellationToken = default) + public async Task GetStatusAsync(CancellationToken cancellationToken = default) { - TaskCompletionSource tcs = new(); - tcs.SetResult(Status); - return tcs.Task; + await Task.Yield(); + return Status; } /// - public StartServerResult RestartServer(string adbPath = null) + public StartServerResult RestartServer() => RestartServer(null); + + /// + public StartServerResult RestartServer(string adbPath) { WasRestarted = true; return StartServer(adbPath, false); @@ -67,12 +69,20 @@ public StartServerResult StartServer(string adbPath, bool restartServerIfNewer) } /// - public Task StartServerAsync(string adbPath, bool restartServerIfNewer, CancellationToken cancellationToken = default) + public async Task StartServerAsync(string adbPath, bool restartServerIfNewer, CancellationToken cancellationToken = default) + { + await Task.Yield(); + return StartServer(adbPath, restartServerIfNewer); + } + + /// + public void StopServer() => Status = Status with { IsRunning = false }; + + /// + public async Task StopServerAsync(CancellationToken cancellationToken = default) { - StartServerResult result = StartServer(adbPath, restartServerIfNewer); - TaskCompletionSource tcs = new(); - tcs.SetResult(result); - return tcs.Task; + await Task.Yield(); + StopServer(); } } } diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbSocket.cs b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbSocket.cs index a2f8d6fc..1d8593c8 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbSocket.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DummyAdbSocket.cs @@ -1,8 +1,6 @@ -using AdvancedSharpAdbClient.Exceptions; -using System; +using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -10,7 +8,10 @@ namespace AdvancedSharpAdbClient.Tests { - internal class DummyAdbSocket : IAdbSocket, IDummyAdbSocket + /// + /// A mock implementation of the class. + /// + internal class DummyAdbSocket : IDummyAdbSocket { /// /// Use this message to cause and to throw @@ -20,8 +21,6 @@ internal class DummyAdbSocket : IAdbSocket, IDummyAdbSocket public DummyAdbSocket() => IsConnected = true; - public Stream ShellStream { get; set; } - public Queue Responses { get; } = new Queue(); public Queue SyncResponses { get; } = new Queue(); @@ -32,9 +31,11 @@ internal class DummyAdbSocket : IAdbSocket, IDummyAdbSocket public Queue ResponseMessages { get; } = new Queue(); - public List Requests { get; } = new List(); + public List Requests { get; } = []; + + public List<(SyncCommand, string)> SyncRequests { get; } = []; - public List<(SyncCommand, string)> SyncRequests { get; } = new List<(SyncCommand, string)>(); + public Queue ShellStreams { get; } = new Queue(); public bool IsConnected { get; set; } @@ -46,20 +47,19 @@ internal class DummyAdbSocket : IAdbSocket, IDummyAdbSocket /// public bool DidReconnect { get; private set; } - public Socket Socket => throw new NotImplementedException(); - - public void Send(byte[] data, int length) => SyncDataSent.Enqueue(data.Take(length).ToArray()); + public void Send(byte[] data, int length) + { + SyncDataSent.Enqueue(data[..length]); + } public void Send(byte[] data, int offset, int length) { - if (offset == 0) - { - Send(data, length); - } - else - { - throw new NotImplementedException(); - } + SyncDataSent.Enqueue(data.AsSpan(offset, length).ToArray()); + } + + public void Send(ReadOnlySpan data) + { + SyncDataSent.Enqueue(data.ToArray()); } public void SendSyncRequest(string command, int value) => SyncRequests.Add((Enum.Parse(command), value.ToString())); @@ -72,31 +72,52 @@ public void Send(byte[] data, int offset, int length) public void SendAdbRequest(string request) => Requests.Add(request); - public int Read(byte[] data) + public int Read(byte[] data, int length) { - byte[] actual = SyncDataReceived.Dequeue(); + Span actual = SyncDataReceived.Dequeue(); + Assert.True(actual.Length >= length); + Assert.True(actual[..length].TryCopyTo(data)); + return length; + } - for (int i = 0; i < data.Length && i < actual.Length; i++) - { - data[i] = actual[i]; - } + public int Read(byte[] data, int offset, int length) + { + Span actual = SyncDataReceived.Dequeue(); + Assert.True(actual.Length >= length); + Assert.True(actual[..length].TryCopyTo(data.AsSpan(offset))); + return length; + } + public int Read(Span data) + { + Span actual = SyncDataReceived.Dequeue(); + Assert.True(actual[..Math.Min(actual.Length, data.Length)].TryCopyTo(data)); return actual.Length; } - public int Read(byte[] data, int length) + public string ReadString() { - byte[] actual = SyncDataReceived.Dequeue(); - - Assert.Equal(actual.Length, length); + if (WaitForNewData) + { + while (ResponseMessages.Count == 0) + { + Thread.Sleep(100); + } + } - Buffer.BlockCopy(actual, 0, data, 0, length); + string message = ResponseMessages.Dequeue(); - return actual.Length; + if (message == ServerDisconnected) + { + SocketException socketException = new(AdbServer.ConnectionReset); + throw new AdbException(socketException.Message, socketException); + } + else + { + return message; + } } - public string ReadString() => ReadStringAsync(CancellationToken.None).Result; - public string ReadSyncString() => ResponseMessages.Dequeue(); public AdbResponse ReadAdbResponse() @@ -108,9 +129,9 @@ public AdbResponse ReadAdbResponse() public Stream GetShellStream() { - if (ShellStream != null) + if (ShellStreams.Dequeue() is Stream actual) { - return ShellStream; + return actual; } else { @@ -147,65 +168,69 @@ public void SetDevice(DeviceData device) public SyncCommand ReadSyncResponse() => SyncResponses.Dequeue(); - public Task SendAsync(byte[] data, int length, CancellationToken cancellationToken = default) + public async Task SendAsync(byte[] data, int length, CancellationToken cancellationToken = default) { + await Task.Yield(); Send(data, length); - return Task.CompletedTask; } - public Task SendAsync(byte[] data, int offset, int length, CancellationToken cancellationToken = default) + public async Task SendAsync(byte[] data, int offset, int length, CancellationToken cancellationToken = default) { + await Task.Yield(); Send(data, offset, length); - return Task.CompletedTask; } - public Task SendSyncRequestAsync(SyncCommand command, string path, int permissions, CancellationToken cancellationToken = default) + public async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) { + await Task.Yield(); + Send(data.Span); + } + + public async Task SendSyncRequestAsync(SyncCommand command, string path, int permissions, CancellationToken cancellationToken = default) + { + await Task.Yield(); SendSyncRequest(command, path, permissions); - return Task.CompletedTask; } - public Task SendSyncRequestAsync(SyncCommand command, string path, CancellationToken cancellationToken = default) + public async Task SendSyncRequestAsync(SyncCommand command, string path, CancellationToken cancellationToken = default) { + await Task.Yield(); SendSyncRequest(command, path); - return Task.CompletedTask; } - public Task SendSyncRequestAsync(SyncCommand command, int length, CancellationToken cancellationToken = default) + public async Task SendSyncRequestAsync(SyncCommand command, int length, CancellationToken cancellationToken = default) { + await Task.Yield(); SendSyncRequest(command, length); - return Task.CompletedTask; } - public Task SendAdbRequestAsync(string request, CancellationToken cancellationToken = default) + public async Task SendAdbRequestAsync(string request, CancellationToken cancellationToken = default) { + await Task.Yield(); SendAdbRequest(request); - return Task.CompletedTask; } - public Task ReadAsync(byte[] data, CancellationToken cancellationToken = default) + public async Task ReadAsync(byte[] data, int length, CancellationToken cancellationToken = default) { - int result = Read(data); - TaskCompletionSource tcs = new(); - tcs.SetResult(result); - return tcs.Task; + await Task.Yield(); + return Read(data, length); } - public Task ReadAsync(byte[] data, int length, CancellationToken cancellationToken = default) + public async ValueTask ReadAsync(Memory data, CancellationToken cancellationToken) { - int result = Read(data, length); - TaskCompletionSource tcs = new(); - tcs.SetResult(result); - return tcs.Task; + await Task.Yield(); + return Read(data.Span); } public async Task ReadStringAsync(CancellationToken cancellationToken = default) { + await Task.Yield(); + if (WaitForNewData) { while (ResponseMessages.Count == 0) { - await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } @@ -223,40 +248,40 @@ public async Task ReadStringAsync(CancellationToken cancellationToken = } } - public Task ReadSyncStringAsync(CancellationToken cancellationToken = default) + public async Task ReadSyncStringAsync(CancellationToken cancellationToken = default) { - string response = ReadSyncString(); - TaskCompletionSource tcs = new(); - tcs.SetResult(response); - return tcs.Task; + await Task.Yield(); + return ReadSyncString(); } - public Task ReadSyncResponseAsync(CancellationToken cancellationToken = default) + public async Task ReadSyncResponseAsync(CancellationToken cancellationToken = default) { - SyncCommand response = ReadSyncResponse(); - TaskCompletionSource tcs = new(); - tcs.SetResult(response); - return tcs.Task; + await Task.Yield(); + return ReadSyncResponse(); } - public Task ReadAdbResponseAsync(CancellationToken cancellationToken = default) + public async Task ReadAdbResponseAsync(CancellationToken cancellationToken = default) { - AdbResponse response = ReadAdbResponse(); - TaskCompletionSource tcs = new(); - tcs.SetResult(response); - return tcs.Task; + await Task.Yield(); + return ReadAdbResponse(); } - public Task SetDeviceAsync(DeviceData device, CancellationToken cancellationToken = default) + public async Task SetDeviceAsync(DeviceData device, CancellationToken cancellationToken = default) { + await Task.Yield(); SetDevice(device); - return Task.CompletedTask; } public void Dispose() => IsConnected = false; public void Close() => IsConnected = false; - public void Reconnect() => DidReconnect = true; + public void Reconnect(bool isForce = false) => DidReconnect = true; + + public async ValueTask ReconnectAsync(bool isForce, CancellationToken cancellationToken = default) + { + await Task.Yield(); + DidReconnect = true; + } } } diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DummySyncService.cs b/AdvancedSharpAdbClient.Tests/Dummys/DummySyncService.cs index fcbffcb4..fd1108ae 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DummySyncService.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DummySyncService.cs @@ -6,56 +6,89 @@ namespace AdvancedSharpAdbClient.Tests { + /// + /// A mock implementation of the class. + /// internal class DummySyncService : ISyncService { - public Dictionary UploadedFiles { get; private set; } = new Dictionary(); + public Dictionary UploadedFiles { get; } = []; - public bool IsOpen => true; + public bool IsOpen { get; private set; } = true; public event EventHandler SyncProgressChanged; - public void Dispose() - { - } - - public IAsyncEnumerable GetDirectoryAsyncListing(string remotePath, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public IEnumerable GetDirectoryListing(string remotePath) => throw new NotImplementedException(); + public void Dispose() => IsOpen = false; - public Task> GetDirectoryListingAsync(string remotePath, CancellationToken cancellationToken) => throw new NotImplementedException(); + public void Open() => IsOpen = true; - public void Open() + public async Task OpenAsync(CancellationToken cancellationToken = default) { + await Task.Yield(); + IsOpen = true; } - public Task OpenAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public void Pull(string remotePath, Stream stream, IProgress progress = null, in bool isCancelled = false) + { + for (int i = 0; i <= 100; i++) + { + SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(i, 100)); + } + } - public void Pull(string remotePath, Stream stream, IProgress progress, CancellationToken cancellationToken = default) => - SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(100, 100)); + public async Task PullAsync(string remotePath, Stream stream, IProgress progress, CancellationToken cancellationToken = default) + { + await Task.Yield(); + for (int i = 0; i <= 100; i++) + { + SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(i, 100)); + } + } - public Task PullAsync(string remotePath, Stream stream, IProgress progress, CancellationToken cancellationToken = default) + public void Push(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress = null, in bool isCancelled = false) { - SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(100, 100)); - return Task.CompletedTask; + for (int i = 0; i <= 100; i++) + { + if (i == 100) + { + UploadedFiles[remotePath] = stream; + } + SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(i, 100)); + } } - public void Push(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress, CancellationToken cancellationToken = default) + public async Task PushAsync(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress, CancellationToken cancellationToken = default) { - SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(0, 100)); - UploadedFiles[remotePath] = stream; - SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(100, 100)); + await Task.Yield(); + for (int i = 0; i <= 100; i++) + { + if (i == 100) + { + UploadedFiles[remotePath] = stream; + } + SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(i, 100)); + } } - public Task PushAsync(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress, CancellationToken cancellationToken = default) + public void Reopen() => IsOpen = true; + + public async Task ReopenAsync(CancellationToken cancellationToken = default) { - SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(0, 100)); - UploadedFiles[remotePath] = stream; - SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(100, 100)); - return Task.CompletedTask; + await Task.Yield(); + IsOpen = true; } - public FileStatistics Stat(string remotePath) => throw new NotImplementedException(); + #region Not Implemented + + IAsyncEnumerable ISyncService.GetDirectoryAsyncListing(string remotePath, CancellationToken cancellationToken) => throw new NotImplementedException(); + + IEnumerable ISyncService.GetDirectoryListing(string remotePath) => throw new NotImplementedException(); + + Task> ISyncService.GetDirectoryListingAsync(string remotePath, CancellationToken cancellationToken) => throw new NotImplementedException(); + + FileStatistics ISyncService.Stat(string remotePath) => throw new NotImplementedException(); + + Task ISyncService.StatAsync(string remotePath, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task StatAsync(string remotePath, CancellationToken cancellationToken) => throw new NotImplementedException(); + #endregion } } diff --git a/AdvancedSharpAdbClient.Tests/Dummys/DummyTcpSocket.cs b/AdvancedSharpAdbClient.Tests/Dummys/DummyTcpSocket.cs index 6d388205..28b64577 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/DummyTcpSocket.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/DummyTcpSocket.cs @@ -7,6 +7,9 @@ namespace AdvancedSharpAdbClient.Tests { + /// + /// A mock implementation of the class. + /// internal class DummyTcpSocket : ITcpSocket { /// @@ -26,30 +29,41 @@ internal class DummyTcpSocket : ITcpSocket public void Close() => Connected = false; public void Connect(EndPoint endPoint) => Connected = true; - public Task ConnectAsync(EndPoint endPoint) + + public async ValueTask ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken = default) { + await Task.Yield(); Connected = true; - return Task.CompletedTask; } - public void Dispose() => Connected = false; - - public Stream GetStream() => OutputStream; + public void Reconnect(bool isForce = false) => Connected = true; - public Task SendAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) + public async ValueTask ReconnectAsync(bool isForce, CancellationToken cancellationToken = default) { - int result = Send(buffer, offset, size, socketFlags); - TaskCompletionSource tcs = new(); - tcs.SetResult(result); - return tcs.Task; + await Task.Yield(); + Connected = true; } + public void Dispose() => Connected = false; + + public Stream GetStream() => OutputStream; + public int Receive(byte[] buffer, int size, SocketFlags socketFlags) => InputStream.Read(buffer, 0, size); - public Task ReceiveAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken) + public int Receive(byte[] buffer, int offset, int size, SocketFlags socketFlags) => InputStream.Read(buffer, offset, size); + + public int Receive(Span buffer, SocketFlags socketFlags) => InputStream.Read(buffer); + + public Task ReceiveAsync(byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => InputStream.ReadAsync(buffer, 0, size, cancellationToken); + + public Task ReceiveAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => InputStream.ReadAsync(buffer, offset, size, cancellationToken); + + public ValueTask ReceiveAsync(Memory buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) => InputStream.ReadAsync(buffer, cancellationToken); + + public int Send(byte[] buffer, int size, SocketFlags socketFlags) { - int value = InputStream.Read(buffer, offset, size); - return Task.FromResult(value); + OutputStream.Write(buffer, 0, size); + return size; } public int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags) @@ -58,8 +72,30 @@ public int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags) return size; } - public byte[] GetBytesSent() => OutputStream.ToArray(); + public int Send(ReadOnlySpan buffer, SocketFlags socketFlags) + { + OutputStream.Write(buffer); + return buffer.Length; + } + + public async Task SendAsync(byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) + { + await OutputStream.WriteAsync(buffer.AsMemory(0, size), cancellationToken).ConfigureAwait(false); + return size; + } - public void Reconnect() => throw new NotImplementedException(); + public async Task SendAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) + { + await OutputStream.WriteAsync(buffer.AsMemory(offset, size), cancellationToken).ConfigureAwait(false); + return size; + } + + public async ValueTask SendAsync(ReadOnlyMemory buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) + { + await OutputStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + return buffer.Length; + } + + public byte[] GetBytesSent() => OutputStream.ToArray(); } } diff --git a/AdvancedSharpAdbClient.Tests/Dummys/IDummyAdbSocket.cs b/AdvancedSharpAdbClient.Tests/Dummys/IDummyAdbSocket.cs index 94dc2da0..febcff1d 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/IDummyAdbSocket.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/IDummyAdbSocket.cs @@ -5,8 +5,6 @@ namespace AdvancedSharpAdbClient.Tests { public interface IDummyAdbSocket : IAdbSocket { - Stream ShellStream { get; set; } - Queue Responses { get; } Queue ResponseMessages { get; } @@ -21,6 +19,8 @@ public interface IDummyAdbSocket : IAdbSocket List<(SyncCommand, string)> SyncRequests { get; } + Queue ShellStreams { get; } + /// /// Gets a value indicating whether the socket reconnected. /// diff --git a/AdvancedSharpAdbClient.Tests/Dummys/TracingAdbSocket.cs b/AdvancedSharpAdbClient.Tests/Dummys/TracingAdbSocket.cs index 936ad99d..918da2b2 100644 --- a/AdvancedSharpAdbClient.Tests/Dummys/TracingAdbSocket.cs +++ b/AdvancedSharpAdbClient.Tests/Dummys/TracingAdbSocket.cs @@ -1,23 +1,15 @@ -using AdvancedSharpAdbClient.Exceptions; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; namespace AdvancedSharpAdbClient.Tests { - internal class TracingAdbSocket : AdbSocket, IDummyAdbSocket + internal class TracingAdbSocket(EndPoint endPoint) : AdbSocket(endPoint), IDummyAdbSocket { - public TracingAdbSocket(EndPoint endPoint) : base(endPoint) - { - } - - public Stream ShellStream { get; set; } - public bool DoDispose { get; set; } public Queue Responses { get; } = new Queue(); @@ -30,9 +22,11 @@ public TracingAdbSocket(EndPoint endPoint) : base(endPoint) public Queue SyncDataSent { get; } = new Queue(); - public List Requests { get; } = new List(); + public List Requests { get; } = []; + + public List<(SyncCommand, string)> SyncRequests { get; } = []; - public List<(SyncCommand, string)> SyncRequests { get; } = new List<(SyncCommand, string)>(); + public Queue ShellStreams { get; } = new Queue(); public bool DidReconnect { get; private set; } @@ -54,7 +48,35 @@ public override int Read(byte[] data, int length) if (trace != null && trace.GetFrames()[1].GetMethod().DeclaringType != typeof(AdbSocket)) { - SyncDataReceived.Enqueue(data.Take(length).ToArray()); + SyncDataReceived.Enqueue(data[..length]); + } + + return read; + } + + public override int Read(byte[] data, int offset, int length) + { + StackTrace trace = new(); + + int read = base.Read(data, offset, length); + + if (trace != null && trace.GetFrames()[1].GetMethod().DeclaringType != typeof(AdbSocket)) + { + SyncDataReceived.Enqueue(data.AsSpan(offset, length).ToArray()); + } + + return read; + } + + public override int Read(Span data) + { + StackTrace trace = new(); + + int read = base.Read(data); + + if (trace != null && trace.GetFrames()[1].GetMethod().DeclaringType != typeof(AdbSocket)) + { + SyncDataReceived.Enqueue(data.ToArray()); } return read; @@ -80,6 +102,20 @@ public override AdbResponse ReadAdbResponse() return exception != null ? throw exception : response; } + public override Stream GetShellStream() + { + StackTrace trace = new(); + + Stream stream = base.GetShellStream(); + + if (trace != null && trace.GetFrames()[1].GetMethod().DeclaringType != typeof(AdbSocket)) + { + ShellStreams.Enqueue(stream); + } + + return stream; + } + public override string ReadString() { string value = base.ReadString(); @@ -94,7 +130,7 @@ public override string ReadSyncString() return value; } - public override async Task ReadStringAsync(CancellationToken cancellationToken) + public override async Task ReadStringAsync(CancellationToken cancellationToken = default) { string value = await base.ReadStringAsync(cancellationToken); ResponseMessages.Enqueue(value); @@ -132,10 +168,9 @@ public override SyncCommand ReadSyncResponse() return response; } - public override void Reconnect() + public override void Reconnect(bool isForce = false) { base.Reconnect(); - DidReconnect = true; } } diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/AdbExceptionTests.cs b/AdvancedSharpAdbClient.Tests/Exceptions/AdbExceptionTests.cs index 8c17e5d8..60baff78 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/AdbExceptionTests.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/AdbExceptionTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System; +using Xunit; namespace AdvancedSharpAdbClient.Exceptions.Tests { @@ -17,6 +18,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new AdbException(message, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new AdbException(info, context)); } diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/CommandAbortingExceptionTests.cs b/AdvancedSharpAdbClient.Tests/Exceptions/CommandAbortingExceptionTests.cs index 2732fdbb..4465153b 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/CommandAbortingExceptionTests.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/CommandAbortingExceptionTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System; +using Xunit; namespace AdvancedSharpAdbClient.Exceptions.Tests { @@ -17,6 +18,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new CommandAbortingException(message, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new CommandAbortingException(info, context)); } diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/DeviceNotFoundException.cs b/AdvancedSharpAdbClient.Tests/Exceptions/DeviceNotFoundException.cs index 7a77ff93..e8ad0d58 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/DeviceNotFoundException.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/DeviceNotFoundException.cs @@ -1,4 +1,5 @@ -using Xunit; +using System; +using Xunit; namespace AdvancedSharpAdbClient.Exceptions.Tests { @@ -13,6 +14,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new DeviceNotFoundException(message, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new DeviceNotFoundException(info, context)); } diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/ExceptionTester.cs b/AdvancedSharpAdbClient.Tests/Exceptions/ExceptionTester.cs index 6d1895b3..9f4f8a25 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/ExceptionTester.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/ExceptionTester.cs @@ -30,6 +30,7 @@ public static void MessageAndInnerConstructorTest(Func con Assert.Equal(inner, ex.InnerException); } + [Obsolete] public static void SerializationConstructorTest(Func constructor) { SerializationInfo info = new(typeof(T), new FormatterConverter()); diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/JavaExceptionTests.cs b/AdvancedSharpAdbClient.Tests/Exceptions/JavaExceptionTests.cs index d0b210d9..371be72a 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/JavaExceptionTests.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/JavaExceptionTests.cs @@ -18,6 +18,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new JavaException(string.Empty, message, string.Empty, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new JavaException(info, context)); @@ -77,7 +78,7 @@ at android.os.Binder.onTransact(Binder.java:824) at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:4644) at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:4513) at android.os.Binder.execTransactInternal(Binder.java:1170) - at android.os.Binder.execTransact(Binder.java:1134)".Split('\r', '\n'); + at android.os.Binder.execTransact(Binder.java:1134)".Split(Extensions.NewLineSeparator); JavaException javaException = JavaException.Parse(lines); diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/PermissionDeniedExceptionTests.cs b/AdvancedSharpAdbClient.Tests/Exceptions/PermissionDeniedExceptionTests.cs index 371a86d6..8f8f78de 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/PermissionDeniedExceptionTests.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/PermissionDeniedExceptionTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System; +using Xunit; namespace AdvancedSharpAdbClient.Exceptions.Tests { @@ -17,6 +18,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new PermissionDeniedException(message, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new PermissionDeniedException(info, context)); } diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/ShellCommandUnresponsiveExceptionTests.cs b/AdvancedSharpAdbClient.Tests/Exceptions/ShellCommandUnresponsiveExceptionTests.cs index 7cd38a6c..36e8190b 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/ShellCommandUnresponsiveExceptionTests.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/ShellCommandUnresponsiveExceptionTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System; +using Xunit; namespace AdvancedSharpAdbClient.Exceptions.Tests { @@ -17,6 +18,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new ShellCommandUnresponsiveException(message, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new ShellCommandUnresponsiveException(info, context)); } diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/UnknownOptionExceptionTests.cs b/AdvancedSharpAdbClient.Tests/Exceptions/UnknownOptionExceptionTests.cs index 58239de8..33123c4f 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/UnknownOptionExceptionTests.cs +++ b/AdvancedSharpAdbClient.Tests/Exceptions/UnknownOptionExceptionTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System; +using Xunit; namespace AdvancedSharpAdbClient.Exceptions.Tests { @@ -17,6 +18,7 @@ public void MessageAndInnerConstructorTest() => ExceptionTester.MessageAndInnerConstructorTest((message, inner) => new UnknownOptionException(message, inner)); [Fact] + [Obsolete] public void SerializationConstructorTest() => ExceptionTester.SerializationConstructorTest((info, context) => new UnknownOptionException(info, context)); } diff --git a/AdvancedSharpAdbClient.Tests/Extensions/AdbCommandLineClientExtensionsTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/AdbCommandLineClientExtensionsTests.cs deleted file mode 100644 index f7449eda..00000000 --- a/AdvancedSharpAdbClient.Tests/Extensions/AdbCommandLineClientExtensionsTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using NSubstitute; -using System; -using System.IO; -using Xunit; - -namespace AdvancedSharpAdbClient.Tests -{ - /// - /// Tests the class. - /// - public class AdbCommandLineClientExtensionsTests - { - [Fact] - public void EnsureIsValidAdbFileNullValueTest() => - _ = Assert.Throws(() => AdbCommandLineClientExtensions.EnsureIsValidAdbFile(null, "adb.exe")); - - [Fact] - public void EnsureIsValidAdbFileInvalidFileTest() - { - IAdbCommandLineClient clientMock = Substitute.For(); - clientMock.IsValidAdbFile(Arg.Any()).Returns(false); - - IAdbCommandLineClient client = clientMock; - - _ = Assert.Throws(() => client.EnsureIsValidAdbFile("xyz.exe")); - } - } -} diff --git a/AdvancedSharpAdbClient.Tests/Extensions/DateTimeExtensionsTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/DateTimeExtensionsTests.cs new file mode 100644 index 00000000..e74c7fc2 --- /dev/null +++ b/AdvancedSharpAdbClient.Tests/Extensions/DateTimeExtensionsTests.cs @@ -0,0 +1,53 @@ +using System; +using Xunit; + +namespace AdvancedSharpAdbClient.Polyfills.Tests +{ + /// + /// Tests the class. + /// + public class DateTimeExtensionsTests + { + /// + /// Tests the method. + /// + [Fact] + public void FromUnixTimeSecondsTest() + { + DateTimeOffset time = new(new DateTime(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc)); + Assert.Equal(time, DateTimeExtensions.FromUnixTimeSeconds(1654085434)); + } + +#if NETFRAMEWORK && !NET46_OR_GREATER + /// + /// Tests the method. + /// + [Fact] + public void ToUnixTimeSecondsTest() + { + DateTimeOffset time = new(new DateTime(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc)); + Assert.Equal(1654085434, time.ToUnixTimeSeconds()); + } +#endif + + /// + /// Tests the method. + /// + [Fact] + public void FromUnixEpochTest() + { + DateTime time = new(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc); + Assert.Equal(time, DateTimeExtensions.FromUnixEpoch(1654085434)); + } + + /// + /// Tests the method. + /// + [Fact] + public void ToUnixEpochTest() + { + DateTime time = new(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc); + Assert.Equal(1654085434, time.ToUnixEpoch()); + } + } +} diff --git a/AdvancedSharpAdbClient.Tests/Extensions/DateTimeHelperTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/DateTimeHelperTests.cs deleted file mode 100644 index ab4c8843..00000000 --- a/AdvancedSharpAdbClient.Tests/Extensions/DateTimeHelperTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Xunit; - -namespace AdvancedSharpAdbClient.Tests -{ - /// - /// Tests the class. - /// - public class DateTimeHelperTests - { - [Fact] - public void ToUnixEpochTest() - { - DateTime time = new(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc); - Assert.Equal(1654085434, time.ToUnixEpoch()); - } - - [Fact] - public void ToDateTimeTest() - { - DateTime time = new(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc); - Assert.Equal(time, ((long)1654085434).ToDateTime()); - } - } -} diff --git a/AdvancedSharpAdbClient.Tests/Extensions/EnumerableExtensionsTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/EnumerableExtensionsTests.cs new file mode 100644 index 00000000..2242e4b0 --- /dev/null +++ b/AdvancedSharpAdbClient.Tests/Extensions/EnumerableExtensionsTests.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace AdvancedSharpAdbClient.Polyfills.Tests +{ + /// + /// Tests the class. + /// + public class EnumerableExtensionsTests + { + /// + /// Tests the method. + /// + [Fact] + public void AddRangeTest() + { + int[] numbs = [6, 7, 8, 9, 10]; + + List list = [1, 2, 3, 4, 5]; + list.AddRange(numbs); + Assert.Equal(10, list.Count); + Assert.Equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], list); + + HashSet hashSet = [1, 2, 3, 4, 5]; + hashSet.AddRange(numbs); + Assert.Equal(10, hashSet.Count); + Assert.Equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], hashSet); + + Collection collection = [1, 2, 3, 4, 5]; + collection.AddRange(numbs); + Assert.Equal(10, collection.Count); + Assert.Equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], collection); + } + + /// + /// Tests the method. + /// + [Fact] + public async void TaskToArrayTest() + { + int[] array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + Task> arrayTask = Extensions.Delay(10).ContinueWith(_ => array.Select(x => x)); + IEnumerable> taskArray = array.Select(x => Extensions.Delay(x).ContinueWith(_ => x)); + Assert.Equal(array, await taskArray.ToArrayAsync()); + Assert.Equal(array, await arrayTask.ToArrayAsync()); + } + } +} diff --git a/AdvancedSharpAdbClient.Tests/Exceptions/ExceptionExtensionsTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/ExceptionExtensionsTests.cs similarity index 95% rename from AdvancedSharpAdbClient.Tests/Exceptions/ExceptionExtensionsTests.cs rename to AdvancedSharpAdbClient.Tests/Extensions/ExceptionExtensionsTests.cs index 288d02f4..4aa180e5 100644 --- a/AdvancedSharpAdbClient.Tests/Exceptions/ExceptionExtensionsTests.cs +++ b/AdvancedSharpAdbClient.Tests/Extensions/ExceptionExtensionsTests.cs @@ -1,7 +1,7 @@ using System; using Xunit; -namespace AdvancedSharpAdbClient.Exceptions.Tests +namespace AdvancedSharpAdbClient.Polyfills.Tests { /// /// Tests the class. @@ -14,7 +14,7 @@ public class ExceptionExtensionsTests [InlineData("name")] public void ThrowIfNullTest(string paramName) { - foreach (object o in new[] { new object(), "", "argument" }) + foreach (object o in new[] { new object(), string.Empty, "argument" }) { ExceptionExtensions.ThrowIfNull(o); ExceptionExtensions.ThrowIfNull(o, nameof(paramName)); diff --git a/AdvancedSharpAdbClient.Tests/Extensions/FactoriesLocker.cs b/AdvancedSharpAdbClient.Tests/Extensions/FactoriesLocker.cs new file mode 100644 index 00000000..c04988f2 --- /dev/null +++ b/AdvancedSharpAdbClient.Tests/Extensions/FactoriesLocker.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AdvancedSharpAdbClient.Tests +{ + /// + /// Locker for the class. + /// + public class FactoriesLocker : IDisposable + { + public static SemaphoreSlim SlimLocker { get; } = new(1, 1); + + public static FactoriesLocker Wait() + { + SlimLocker.Wait(); + return new FactoriesLocker(); + } + + public static async Task WaitAsync() + { + await SlimLocker.WaitAsync(); + return new FactoriesLocker(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + SlimLocker.Release(); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/AdvancedSharpAdbClient.Tests/Extensions/SyncCommandConverterTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/SyncCommandConverterTests.cs index e0e65c15..bc4511e0 100644 --- a/AdvancedSharpAdbClient.Tests/Extensions/SyncCommandConverterTests.cs +++ b/AdvancedSharpAdbClient.Tests/Extensions/SyncCommandConverterTests.cs @@ -10,18 +10,30 @@ public class SyncCommandConverterTests { [Fact] public void GetCommandNullTest() => - _ = Assert.Throws(() => SyncCommandConverter.GetCommand(null)); + _ = Assert.Throws(() => SyncCommandConverter.GetCommand(null)); [Fact] public void GetCommandInvalidNumberOfBytesTest() => - _ = Assert.Throws(() => SyncCommandConverter.GetCommand(new byte[] { })); + _ = Assert.Throws(() => SyncCommandConverter.GetCommand([])); [Fact] public void GetCommandInvalidCommandTest() => - _ = Assert.Throws(() => SyncCommandConverter.GetCommand(new byte[] { (byte)'Q', (byte)'M', (byte)'T', (byte)'V' })); + _ = Assert.Throws(() => SyncCommandConverter.GetCommand("QMTV"u8)); [Fact] public void GetBytesInvalidCommandTest() => _ = Assert.Throws(() => SyncCommandConverter.GetBytes((SyncCommand)99)); + + [Fact] + public void SyncCommandConverterTest() + { + SyncCommand[] commands = Enum.GetValues(); + foreach (SyncCommand command in commands) + { + byte[] bytes = SyncCommandConverter.GetBytes(command); + Assert.Equal(4, bytes.Length); + Assert.Equal(command, SyncCommandConverter.GetCommand(bytes)); + } + } } } diff --git a/AdvancedSharpAdbClient.Tests/Extensions/UtilitiesTests.cs b/AdvancedSharpAdbClient.Tests/Extensions/UtilitiesTests.cs deleted file mode 100644 index 1af7da56..00000000 --- a/AdvancedSharpAdbClient.Tests/Extensions/UtilitiesTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Xunit; - -namespace AdvancedSharpAdbClient.Tests -{ - /// - /// Tests the class. - /// - public class UtilitiesTests - { - [Fact] - public void TryParseTest() - { - Assert.True(Utilities.TryParse("BootLoader", false, out DeviceState result)); - Assert.Equal(DeviceState.BootLoader, result); - Assert.True(Utilities.TryParse("Bootloader", true, out result)); - Assert.Equal(DeviceState.BootLoader, result); - Assert.False(Utilities.TryParse("Bootloader", false, out _)); - Assert.False(Utilities.TryParse("Reset", true, out _)); - } - - [Fact] - public void IsNullOrWhiteSpaceTest() - { - Assert.True(" ".IsNullOrWhiteSpace()); - Assert.False(" test ".IsNullOrWhiteSpace()); - } - - [Fact] - public void JoinTest() => - Assert.Equal("Hello World!", Utilities.Join(" ", new string[] { "Hello", "World!" })); - - [Fact] - public void FromUnixTimeSecondsTest() - { - DateTimeOffset time = new(new DateTime(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc)); - Assert.Equal(time, Utilities.FromUnixTimeSeconds(1654085434)); - } - - [Fact] - public void ToUnixTimeSecondsTest() - { - DateTimeOffset time = new(new DateTime(2022, 6, 1, 12, 10, 34, DateTimeKind.Utc)); - Assert.Equal(1654085434, time.ToUnixTimeSeconds()); - } - } -} diff --git a/AdvancedSharpAdbClient.Tests/Logs/Dummys/DummyLogger.cs b/AdvancedSharpAdbClient.Tests/Logs/Dummys/DummyLogger.cs new file mode 100644 index 00000000..25f6e9e7 --- /dev/null +++ b/AdvancedSharpAdbClient.Tests/Logs/Dummys/DummyLogger.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace AdvancedSharpAdbClient.Logs.Tests +{ + public class DummyLogger(string name) : ILogger + { + public string Name { get; } = name; + + public List LogMessages { get; } = []; + + public void Log(LogLevel logLevel, Exception exception, string message, params object[] args) => + LogMessages.Add(new LogMessage(logLevel, exception, message, args)); + } + + public class DummyLogger : DummyLogger, ILogger + { + public Type Type { get; } = typeof(T); + + public DummyLogger() : base(typeof(T).Name) { } + } + + public record class LogMessage(LogLevel LogLevel, Exception Exception, string Message, params object[] Args) + { + public bool HasFormat => Args.Length > 0; + public bool HasException => Exception != null; + public string FormattedMessage => HasFormat ? string.Format(Message, Args) : Message; + } +} diff --git a/AdvancedSharpAdbClient.Tests/Logs/Dummys/DummyLoggerFactory.cs b/AdvancedSharpAdbClient.Tests/Logs/Dummys/DummyLoggerFactory.cs new file mode 100644 index 00000000..874de3e8 --- /dev/null +++ b/AdvancedSharpAdbClient.Tests/Logs/Dummys/DummyLoggerFactory.cs @@ -0,0 +1,9 @@ +namespace AdvancedSharpAdbClient.Logs.Tests +{ + public class DummyLoggerFactory : ILoggerFactory + { + public ILogger CreateLogger(string categoryName) => new DummyLogger(categoryName); + + public ILogger CreateLogger() => new DummyLogger(); + } +} diff --git a/AdvancedSharpAdbClient.Tests/Logs/LoggerTests.cs b/AdvancedSharpAdbClient.Tests/Logs/LogTests.cs similarity index 80% rename from AdvancedSharpAdbClient.Tests/Logs/LoggerTests.cs rename to AdvancedSharpAdbClient.Tests/Logs/LogTests.cs index d4231b38..d75c7f19 100644 --- a/AdvancedSharpAdbClient.Tests/Logs/LoggerTests.cs +++ b/AdvancedSharpAdbClient.Tests/Logs/LogTests.cs @@ -1,17 +1,16 @@ using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.IO; -using System.Threading; using Xunit; namespace AdvancedSharpAdbClient.Logs.Tests { - public class LoggerTests + public class LogTests { [Fact] public void ReadLogTest() { - using Stream stream = File.OpenRead(@"Assets/logcat.bin"); + using FileStream stream = File.OpenRead(@"Assets/logcat.bin"); using ShellStream shellStream = new(stream, false); LogReader reader = new(shellStream); @@ -40,13 +39,13 @@ public void ReadLogTest() [Fact] public async void ReadLogAsyncTest() { - await using Stream stream = File.OpenRead(@"Assets/logcat.bin"); + await using FileStream stream = File.OpenRead(@"Assets/logcat.bin"); await using ShellStream shellStream = new(stream, false); LogReader reader = new(shellStream); // This stream contains 3 log entries. Read & validate the first one, // read the next two ones (to be sure all data is read correctly). - LogEntry log = await reader.ReadEntryAsync(CancellationToken.None); + LogEntry log = await reader.ReadEntryAsync(); Assert.IsType(log); @@ -62,8 +61,8 @@ public async void ReadLogAsyncTest() Assert.Equal("ActivityManager", androidLog.Tag); Assert.Equal("Start proc com.google.android.gm for broadcast com.google.android.gm/.widget.GmailWidgetProvider: pid=7026 uid=10066 gids={50066, 9997, 3003, 1028, 1015} abi=x86", androidLog.Message); - Assert.NotNull(await reader.ReadEntryAsync(CancellationToken.None)); - Assert.NotNull(await reader.ReadEntryAsync(CancellationToken.None)); + Assert.NotNull(await reader.ReadEntryAsync()); + Assert.NotNull(await reader.ReadEntryAsync()); } [Fact] @@ -71,7 +70,7 @@ public void ReadEventLogTest() { // The data in this stream was read using a ShellStream, so the CRLF fixing // has already taken place. - using Stream stream = File.OpenRead(@"Assets/logcatevents.bin"); + using FileStream stream = File.OpenRead(@"Assets/logcatevents.bin"); LogReader reader = new(stream); LogEntry entry = reader.ReadEntry(); @@ -88,9 +87,9 @@ public void ReadEventLogTest() Assert.NotNull(eventLog.Values); Assert.Single(eventLog.Values); Assert.NotNull(eventLog.Values[0]); - Assert.IsType>(eventLog.Values[0]); + Assert.IsType>(eventLog.Values[0]); - Collection list = (Collection)eventLog.Values[0]; + List list = (List)eventLog.Values[0]; Assert.Equal(3, list.Count); Assert.Equal(0, list[0]); Assert.Equal(19512, list[1]); @@ -105,9 +104,9 @@ public async void ReadEventLogAsyncTest() { // The data in this stream was read using a ShellStream, so the CRLF fixing // has already taken place. - await using Stream stream = File.OpenRead(@"Assets/logcatevents.bin"); + await using FileStream stream = File.OpenRead(@"Assets/logcatevents.bin"); LogReader reader = new(stream); - LogEntry entry = await reader.ReadEntryAsync(CancellationToken.None); + LogEntry entry = await reader.ReadEntryAsync(); Assert.IsType(entry); Assert.Equal(707, entry.ProcessId); @@ -122,16 +121,16 @@ public async void ReadEventLogAsyncTest() Assert.NotNull(eventLog.Values); Assert.Single(eventLog.Values); Assert.NotNull(eventLog.Values[0]); - Assert.IsType>(eventLog.Values[0]); + Assert.IsType>(eventLog.Values[0]); - Collection list = (Collection)eventLog.Values[0]; + List list = (List)eventLog.Values[0]; Assert.Equal(3, list.Count); Assert.Equal(0, list[0]); Assert.Equal(19512, list[1]); Assert.Equal("com.amazon.kindle", list[2]); - entry = await reader.ReadEntryAsync(CancellationToken.None); - entry = await reader.ReadEntryAsync(CancellationToken.None); + entry = await reader.ReadEntryAsync(); + entry = await reader.ReadEntryAsync(); } } } diff --git a/AdvancedSharpAdbClient.Tests/Models/AdbResponseTests.cs b/AdvancedSharpAdbClient.Tests/Models/AdbResponseTests.cs index b7001673..e896cc6b 100644 --- a/AdvancedSharpAdbClient.Tests/Models/AdbResponseTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/AdbResponseTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the class. @@ -10,18 +10,16 @@ public class AdbResponseTests [Fact] public void EqualsTest() { - AdbResponse first = new() + AdbResponse first = new("Hi") { IOSuccess = false, - Message = "Hi", Okay = false, Timeout = false }; - AdbResponse second = new() + AdbResponse second = new("Hi") { IOSuccess = true, - Message = "Hi", Okay = false, Timeout = false }; @@ -34,18 +32,16 @@ public void EqualsTest() [Fact] public void GetHashCodeTest() { - AdbResponse first = new() + AdbResponse first = new("Hi") { IOSuccess = false, - Message = "Hi", Okay = false, Timeout = false }; - AdbResponse second = new() + AdbResponse second = new("Hi") { IOSuccess = false, - Message = "Hi", Okay = false, Timeout = false }; diff --git a/AdvancedSharpAdbClient.Tests/Models/AdbServerStatusTests.cs b/AdvancedSharpAdbClient.Tests/Models/AdbServerStatusTests.cs index c6bca265..624ee170 100644 --- a/AdvancedSharpAdbClient.Tests/Models/AdbServerStatusTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/AdbServerStatusTests.cs @@ -1,7 +1,7 @@ using System; using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the class. @@ -11,10 +11,10 @@ public class AdbServerStatusTests [Fact] public void ToStringTest() { - AdbServerStatus s = new(true, new Version(1, 0, 32)); - Assert.Equal("Version 1.0.32 of the adb daemon is running.", s.ToString()); - s.IsRunning = false; - Assert.Equal("The adb daemon is not running.", s.ToString()); + AdbServerStatus status = new(true, new Version(1, 0, 32)); + Assert.Equal("Version 1.0.32 of the adb daemon is running.", status.ToString()); + status = status with { IsRunning = false }; + Assert.Equal("The adb daemon is not running.", status.ToString()); } } } diff --git a/AdvancedSharpAdbClient.Tests/Models/AreaTests.cs b/AdvancedSharpAdbClient.Tests/Models/AreaTests.cs deleted file mode 100644 index 9ee87729..00000000 --- a/AdvancedSharpAdbClient.Tests/Models/AreaTests.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System; -using System.Drawing; -using System.Globalization; -using Xunit; - -namespace AdvancedSharpAdbClient.Tests -{ - /// - /// Tests the struct. - /// - public class AreaTests - { - [Fact] - public void DefaultConstructorTest() - { - Assert.Equal(Area.Empty, new Area()); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] - [InlineData(int.MaxValue, 0, int.MinValue, 0)] - [InlineData(0, 0, 0, 0)] - [InlineData(0, int.MinValue, 0, int.MaxValue)] - public void NonDefaultConstructorTest(int x, int y, int width, int height) - { - Area rect1 = new(x, y, width, height); - Area rect2 = new(new Cords(x, y), new Size(width, height)); - - Assert.Equal(rect1, rect2); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] - [InlineData(int.MaxValue, 0, int.MinValue, 0)] - [InlineData(0, 0, 0, 0)] - [InlineData(0, int.MinValue, 0, int.MaxValue)] - public void FromLTRBTest(int left, int top, int right, int bottom) - { - Area rect1 = new(left, top, unchecked(right - left), unchecked(bottom - top)); - Area rect2 = Area.FromLTRB(left, top, right, bottom); - - Assert.Equal(rect1, rect2); - } - - [Fact] - public void EmptyTest() - { - Assert.True(Area.Empty.IsEmpty); - Assert.True(new Area(0, 0, 0, 0).IsEmpty); - Assert.True(new Area().IsEmpty); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] - [InlineData(int.MaxValue, 0, int.MinValue, 0)] - [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] - [InlineData(0, int.MinValue, 0, int.MaxValue)] - public void NonEmptyTest(int x, int y, int width, int height) - { - Assert.False(new Area(x, y, width, height).IsEmpty); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] - [InlineData(int.MaxValue, 0, int.MinValue, 0)] - [InlineData(0, 0, 0, 0)] - [InlineData(0, int.MinValue, 0, int.MaxValue)] - [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] - public void DimensionsTest(int x, int y, int width, int height) - { - Area rect = new(x, y, width, height); - Assert.Equal(new Cords(x, y), rect.Location); - Assert.Equal(new Size(width, height), rect.Size); - - Assert.Equal(x, rect.X); - Assert.Equal(y, rect.Y); - Assert.Equal(width, rect.Width); - Assert.Equal(height, rect.Height); - Assert.Equal(x, rect.Left); - Assert.Equal(y, rect.Top); - Assert.Equal(unchecked(x + width), rect.Right); - Assert.Equal(unchecked(y + height), rect.Bottom); - - Cords p = new(width, height); - Size s = new(x, y); - rect.Location = p; - rect.Size = s; - - Assert.Equal(p, rect.Location); - Assert.Equal(s, rect.Size); - - Assert.Equal(width, rect.X); - Assert.Equal(height, rect.Y); - Assert.Equal(x, rect.Width); - Assert.Equal(y, rect.Height); - Assert.Equal(width, rect.Left); - Assert.Equal(height, rect.Top); - Assert.Equal(unchecked(x + width), rect.Right); - Assert.Equal(unchecked(y + height), rect.Bottom); - } - - [Theory] - [InlineData(0, 0)] - [InlineData(int.MaxValue, int.MinValue)] - public static void LocationSetTest(int x, int y) - { - Cords Cords = new(x, y); - Area rect = new(10, 10, 10, 10) - { - Location = Cords - }; - Assert.Equal(Cords, rect.Location); - Assert.Equal(Cords.X, rect.X); - Assert.Equal(Cords.Y, rect.Y); - } - - [Theory] - [InlineData(0, 0)] - [InlineData(int.MaxValue, int.MinValue)] - public static void SizeSetTest(int x, int y) - { - Size size = new(x, y); - Area rect = new(10, 10, 10, 10) - { - Size = size - }; - Assert.Equal(size, rect.Size); - Assert.Equal(size.Width, rect.Width); - Assert.Equal(size.Height, rect.Height); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] - [InlineData(int.MaxValue, 0, int.MinValue, 0)] - [InlineData(0, int.MinValue, 0, int.MaxValue)] - [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] - public void EqualityTest(int x, int y, int width, int height) - { - Area rect1 = new(x, y, width, height); - Area rect2 = new(width / 2, height / 2, x, y); - - Assert.True(rect1 != rect2); - Assert.False(rect1 == rect2); - Assert.False(rect1.Equals(rect2)); - Assert.False(rect1.Equals((object)rect2)); - } - - [Fact] - public static void EqualityTest_NotArea() - { - Area Area = new(0, 0, 0, 0); - Assert.False(Area.Equals(null)); - Assert.False(Area.Equals(0)); - Assert.False(Area.Equals(new RectangleF(0, 0, 0, 0))); - } - - [Fact] - public static void GetHashCodeTest() - { - Area rect1 = new(10, 10, 10, 10); - Area rect2 = new(10, 10, 10, 10); - Assert.Equal(rect1.GetHashCode(), rect2.GetHashCode()); - Assert.NotEqual(rect1.GetHashCode(), new Area(20, 10, 10, 10).GetHashCode()); - Assert.NotEqual(rect1.GetHashCode(), new Area(10, 20, 10, 10).GetHashCode()); - Assert.NotEqual(rect1.GetHashCode(), new Area(10, 10, 20, 10).GetHashCode()); - Assert.NotEqual(rect1.GetHashCode(), new Area(10, 10, 10, 20).GetHashCode()); - } - - [Theory] - [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MinValue)] - [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MaxValue)] - [InlineData(0, 0, 0, 0)] - public void AreaFConversionTest(float x, float y, float width, float height) - { - RectangleF rect = new(x, y, width, height); - Area rCeiling, rTruncate, rRound; - - unchecked - { - rCeiling = new Area((int)Math.Ceiling(x), (int)Math.Ceiling(y), - (int)Math.Ceiling(width), (int)Math.Ceiling(height)); - rTruncate = new Area((int)x, (int)y, (int)width, (int)height); - rRound = new Area((int)Math.Round(x), (int)Math.Round(y), - (int)Math.Round(width), (int)Math.Round(height)); - } - - Assert.Equal(rCeiling, Area.Ceiling(rect)); - Assert.Equal(rTruncate, Area.Truncate(rect)); - Assert.Equal(rRound, Area.Round(rect)); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] - [InlineData(0, int.MinValue, int.MaxValue, 0)] - public void ContainsTest(int x, int y, int width, int height) - { - Area rect = new(unchecked((2 * x) - width), unchecked((2 * y) - height), width, height); - Cords p = new(x, y); - Area r = new(x, y, width / 2, height / 2); - - Assert.False(rect.Contains(x, y)); - Assert.False(rect.Contains(p)); - Assert.False(rect.Contains(r)); - } - - [Theory] - [InlineData(0, 0, 0, 0)] - [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] - [InlineData(0, int.MinValue, int.MaxValue, 0)] - public void InflateTest(int x, int y, int width, int height) - { - Area inflatedRect, rect = new(x, y, width, height); - unchecked - { - inflatedRect = new Area(x - width, y - height, width + (2 * width), height + (2 * height)); - } - - Assert.Equal(inflatedRect, Area.Inflate(rect, width, height)); - - rect.Inflate(width, height); - Assert.Equal(inflatedRect, rect); - - Size s = new(x, y); - unchecked - { - inflatedRect = new Area(rect.X - x, rect.Y - y, rect.Width + (2 * x), rect.Height + (2 * y)); - } - - rect.Inflate(s); - Assert.Equal(inflatedRect, rect); - } - - [Theory] - [InlineData(0, 0, 0, 0)] - [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] - [InlineData(0, int.MinValue, int.MaxValue, 0)] - public void IntersectTest(int x, int y, int width, int height) - { - Area rect = new(x, y, width, height); - Area expectedRect = Area.Intersect(rect, rect); - rect.Intersect(rect); - Assert.Equal(expectedRect, rect); - Assert.False(rect.IntersectsWith(expectedRect)); - } - - [Fact] - public static void Intersect_IntersectingAreas_Test() - { - Area rect1 = new(0, 0, 5, 5); - Area rect2 = new(1, 1, 3, 3); - Area expected = new(1, 1, 3, 3); - - Assert.Equal(expected, Area.Intersect(rect1, rect2)); - } - - [Theory] - [InlineData(0, 0, 0, 0)] - [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] - [InlineData(int.MaxValue, 0, 0, int.MaxValue)] - [InlineData(0, int.MinValue, int.MaxValue, 0)] - public void UnionTest(int x, int y, int width, int height) - { - Area a = new(x, y, width, height); - Area b = new(width, height, x, y); - - int x1 = Math.Min(a.X, b.X); - int x2 = Math.Max(a.X + a.Width, b.X + b.Width); - int y1 = Math.Min(a.Y, b.Y); - int y2 = Math.Max(a.Y + a.Height, b.Y + b.Height); - - Area expectedArea = new(x1, y1, x2 - x1, y2 - y1); - - Assert.Equal(expectedArea, Area.Union(a, b)); - } - - [Theory] - [InlineData(0, 0, 0, 0)] - [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] - [InlineData(int.MaxValue, 0, 0, int.MaxValue)] - [InlineData(0, int.MinValue, int.MaxValue, 0)] - public void OffsetTest(int x, int y, int width, int height) - { - Area r1 = new(x, y, width, height); - Area expectedRect = new(x + width, y + height, width, height); - Cords p = new(width, height); - - r1.Offset(p); - Assert.Equal(expectedRect, r1); - - expectedRect.Offset(p); - r1.Offset(width, height); - Assert.Equal(expectedRect, r1); - } - - [Theory] - [InlineData(0, 0, 0, 0)] - [InlineData(5, -5, 0, 1)] - public void ToStringTest(int x, int y, int width, int height) - { - Area r = new(x, y, width, height); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "{{X={0},Y={1},Width={2},Height={3}}}", r.X, r.Y, r.Width, r.Height), r.ToString()); - } - } -} diff --git a/AdvancedSharpAdbClient.Tests/Models/CordsTests.cs b/AdvancedSharpAdbClient.Tests/Models/CordsTests.cs deleted file mode 100644 index 194bc1f5..00000000 --- a/AdvancedSharpAdbClient.Tests/Models/CordsTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Drawing; -using System.Globalization; -using Xunit; - -namespace AdvancedSharpAdbClient.Tests -{ - /// - /// Tests the struct. - /// - public class CordsTests - { - [Fact] - public void DefaultConstructorTest() - { - Assert.Equal(Cords.Empty, new Cords()); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void NonDefaultConstructorTest(int x, int y) - { - Cords p1 = new(x, y); - Cords p2 = new(new Size(x, y)); - - Assert.Equal(p1, p2); - } - - [Theory] - [InlineData(int.MaxValue)] - [InlineData(int.MinValue)] - [InlineData(0)] - public void SingleIntConstructorTest(int x) - { - Cords p1 = new(x); - Cords p2 = new(unchecked((short)(x & 0xFFFF)), unchecked((short)((x >> 16) & 0xFFFF))); - - Assert.Equal(p1, p2); - } - - [Fact] - public void IsEmptyDefaultsTest() - { - Assert.True(Cords.Empty.IsEmpty); - Assert.True(new Cords().IsEmpty); - Assert.True(new Cords(0, 0).IsEmpty); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - public void IsEmptyRandomTest(int x, int y) - { - Assert.False(new Cords(x, y).IsEmpty); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void CoordinatesTest(int x, int y) - { - Cords p = new(x, y); - Assert.Equal(x, p.X); - Assert.Equal(y, p.Y); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void CordsFConversionTest(int x, int y) - { - PointF p = new Cords(x, y); - Assert.Equal(new PointF(x, y), p); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void SizeConversionTest(int x, int y) - { - Size sz = (Size)new Cords(x, y); - Assert.Equal(new Size(x, y), sz); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void ArithmeticTest(int x, int y) - { - Cords addExpected, subExpected, p = new(x, y); - Size s = new(y, x); - - unchecked - { - addExpected = new Cords(x + y, y + x); - subExpected = new Cords(x - y, y - x); - } - - Assert.Equal(addExpected, p + s); - Assert.Equal(subExpected, p - s); - Assert.Equal(addExpected, Cords.Add(p, s)); - Assert.Equal(subExpected, Cords.Subtract(p, s)); - } - - [Theory] - [InlineData(float.MaxValue, float.MinValue)] - [InlineData(float.MinValue, float.MinValue)] - [InlineData(float.MaxValue, float.MaxValue)] - [InlineData(0, 0)] - public void CordsFMathematicalTest(float x, float y) - { - PointF pf = new(x, y); - Cords pCeiling, pTruncate, pRound; - - unchecked - { - pCeiling = new Cords((int)Math.Ceiling(x), (int)Math.Ceiling(y)); - pTruncate = new Cords((int)x, (int)y); - pRound = new Cords((int)Math.Round(x), (int)Math.Round(y)); - } - - Assert.Equal(pCeiling, Cords.Ceiling(pf)); - Assert.Equal(pRound, Cords.Round(pf)); - Assert.Equal(pTruncate, Cords.Truncate(pf)); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void OffsetTest(int x, int y) - { - Cords p1 = new(x, y); - Cords p2 = new(y, x); - - p1.Offset(p2); - - Assert.Equal(unchecked(p2.X + p2.Y), p1.X); - Assert.Equal(p1.X, p1.Y); - - p2.Offset(x, y); - Assert.Equal(p1, p2); - } - - [Theory] - [InlineData(int.MaxValue, int.MinValue)] - [InlineData(int.MinValue, int.MinValue)] - [InlineData(int.MaxValue, int.MaxValue)] - [InlineData(0, 0)] - public void EqualityTest(int x, int y) - { - Cords p1 = new(x, y); - Cords p2 = new((x / 2) - 1, (y / 2) - 1); - Cords p3 = new(x, y); - - Assert.True(p1 == p3); - Assert.True(p1 != p2); - Assert.True(p2 != p3); - - Assert.True(p1.Equals(p3)); - Assert.False(p1.Equals(p2)); - Assert.False(p2.Equals(p3)); - - Assert.True(p1.Equals((object)p3)); - Assert.False(p1.Equals((object)p2)); - Assert.False(p2.Equals((object)p3)); - - Assert.Equal(p1.GetHashCode(), p3.GetHashCode()); - } - - [Fact] - public static void EqualityTest_NotCords() - { - Cords Cords = new(0, 0); - Assert.False(Cords.Equals(null)); - Assert.False(Cords.Equals(0)); - Assert.False(Cords.Equals(new PointF(0, 0))); - } - - [Fact] - public static void GetHashCodeTest() - { - Cords Cords = new(10, 10); - Assert.Equal(Cords.GetHashCode(), new Cords(10, 10).GetHashCode()); - Assert.NotEqual(Cords.GetHashCode(), new Cords(20, 10).GetHashCode()); - Assert.NotEqual(Cords.GetHashCode(), new Cords(10, 20).GetHashCode()); - } - - [Theory] - [InlineData(0, 0, 0, 0)] - [InlineData(1, -2, 3, -4)] - public void ConversionTest(int x, int y, int width, int height) - { - Area rect = new(x, y, width, height); - RectangleF rectF = rect; - Assert.Equal(x, rectF.X); - Assert.Equal(y, rectF.Y); - Assert.Equal(width, rectF.Width); - Assert.Equal(height, rectF.Height); - } - - [Theory] - [InlineData(0, 0)] - [InlineData(5, -5)] - public void ToStringTest(int x, int y) - { - Cords p = new(x, y); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "{{X={0},Y={1}}}", p.X, p.Y), p.ToString()); - } - } -} diff --git a/AdvancedSharpAdbClient.Tests/Models/DeviceDataTests.cs b/AdvancedSharpAdbClient.Tests/Models/DeviceDataTests.cs index 6b413da8..cf001fa1 100644 --- a/AdvancedSharpAdbClient.Tests/Models/DeviceDataTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/DeviceDataTests.cs @@ -1,7 +1,7 @@ using System; using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the class. diff --git a/AdvancedSharpAdbClient.Tests/Models/ForwardDataTests.cs b/AdvancedSharpAdbClient.Tests/Models/ForwardDataTests.cs index ae75dcf9..30958db1 100644 --- a/AdvancedSharpAdbClient.Tests/Models/ForwardDataTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/ForwardDataTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the class. diff --git a/AdvancedSharpAdbClient.Tests/Models/ForwardSpecTests.cs b/AdvancedSharpAdbClient.Tests/Models/ForwardSpecTests.cs index b3659a74..bc766576 100644 --- a/AdvancedSharpAdbClient.Tests/Models/ForwardSpecTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/ForwardSpecTests.cs @@ -1,7 +1,7 @@ using System; using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the class. @@ -13,7 +13,6 @@ public void TcpTest() { ForwardSpec value = ForwardSpec.Parse("tcp:1234"); - Assert.NotNull(value); Assert.Equal(ForwardProtocol.Tcp, value.Protocol); Assert.Equal(1234, value.Port); Assert.Equal(0, value.ProcessId); @@ -27,7 +26,6 @@ public void SocketText() { ForwardSpec value = ForwardSpec.Parse("localabstract:/tmp/1234"); - Assert.NotNull(value); Assert.Equal(ForwardProtocol.LocalAbstract, value.Protocol); Assert.Equal(0, value.Port); Assert.Equal(0, value.ProcessId); @@ -41,7 +39,6 @@ public void JdwpTest() { ForwardSpec value = ForwardSpec.Parse("jdwp:1234"); - Assert.NotNull(value); Assert.Equal(ForwardProtocol.JavaDebugWireProtocol, value.Protocol); Assert.Equal(0, value.Port); Assert.Equal(1234, value.ProcessId); diff --git a/AdvancedSharpAdbClient.Tests/Models/FramebufferHeaderTests.cs b/AdvancedSharpAdbClient.Tests/Models/FramebufferHeaderTests.cs index b407cedb..e1440868 100644 --- a/AdvancedSharpAdbClient.Tests/Models/FramebufferHeaderTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/FramebufferHeaderTests.cs @@ -2,11 +2,10 @@ using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Runtime.InteropServices; using System.Runtime.Versioning; using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the struct. @@ -18,7 +17,7 @@ public void ReadFramebufferTest() { byte[] data = File.ReadAllBytes("Assets/framebufferheader-v1.bin"); - FramebufferHeader header = FramebufferHeader.Read(data); + FramebufferHeader header = [.. data]; Assert.Equal(8u, header.Alpha.Length); Assert.Equal(24u, header.Alpha.Offset); @@ -34,6 +33,13 @@ public void ReadFramebufferTest() Assert.Equal(1440u, header.Width); Assert.Equal(1u, header.Version); Assert.Equal(0u, header.ColorSpace); + + for (int i = 0; i < header.Count; i++) + { + Assert.Equal(data[i], header[i]); + } + + Assert.Equal(data.AsSpan(), [.. header]); } [Fact] @@ -41,7 +47,7 @@ public void ReadFramebufferV2Test() { byte[] data = File.ReadAllBytes("Assets/framebufferheader-v2.bin"); - FramebufferHeader header = FramebufferHeader.Read(data); + FramebufferHeader header = [.. data]; Assert.Equal(8u, header.Alpha.Length); Assert.Equal(24u, header.Alpha.Offset); @@ -57,14 +63,20 @@ public void ReadFramebufferV2Test() Assert.Equal(1080u, header.Width); Assert.Equal(2u, header.Version); Assert.Equal(0u, header.ColorSpace); + + for (int i = 0; i < header.Count; i++) + { + Assert.Equal(data[i], header[i]); + } + + Assert.Equal(data.AsSpan(), [.. header]); } +#if WINDOWS [Fact] [SupportedOSPlatform("windows")] public void ToImageTest() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; } - byte[] data = File.ReadAllBytes("Assets/framebufferheader.bin"); FramebufferHeader header = FramebufferHeader.Read(data); byte[] framebuffer = File.ReadAllBytes("Assets/framebuffer.bin"); @@ -86,15 +98,14 @@ public void ToImageTest() [SupportedOSPlatform("windows")] public void ToImageEmptyTest() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; } - byte[] data = File.ReadAllBytes("Assets/framebufferheader-empty.bin"); FramebufferHeader header = FramebufferHeader.Read(data); - byte[] framebuffer = Array.Empty(); + byte[] framebuffer = []; Bitmap image = header.ToImage(framebuffer); Assert.Null(image); } +#endif } } diff --git a/AdvancedSharpAdbClient.Tests/Models/FramebufferTests.cs b/AdvancedSharpAdbClient.Tests/Models/FramebufferTests.cs index e0d25840..be8e0a92 100644 --- a/AdvancedSharpAdbClient.Tests/Models/FramebufferTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/FramebufferTests.cs @@ -3,10 +3,9 @@ using System.Drawing.Imaging; using System.IO; using System.Net; -using System.Runtime.InteropServices; using Xunit; -namespace AdvancedSharpAdbClient.Tests.Models +namespace AdvancedSharpAdbClient.Models.Tests { /// /// Tests the class. @@ -44,9 +43,8 @@ public void RefreshTest() socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebufferheader.bin")); socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebuffer.bin")); - using Framebuffer framebuffer = new(device); + using Framebuffer framebuffer = new(device, (endPoint) => socket); - Factories.AdbSocketFactory = (endPoint) => socket; framebuffer.Refresh(); Assert.NotNull(framebuffer); @@ -70,21 +68,20 @@ public void RefreshTest() Assert.Equal(1u, header.Version); Assert.Equal(0u, header.ColorSpace); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - using Bitmap image = (Bitmap)framebuffer; - Assert.NotNull(image); - Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); - - Assert.Equal(1, image.Width); - Assert.Equal(1, image.Height); - - Color pixel = image.GetPixel(0, 0); - Assert.Equal(0x35, pixel.R); - Assert.Equal(0x4a, pixel.G); - Assert.Equal(0x4c, pixel.B); - Assert.Equal(0xff, pixel.A); - } +#if WINDOWS + using Bitmap image = (Bitmap)framebuffer; + Assert.NotNull(image); + Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); + + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + + Color pixel = image.GetPixel(0, 0); + Assert.Equal(0x35, pixel.R); + Assert.Equal(0x4a, pixel.G); + Assert.Equal(0x4c, pixel.B); + Assert.Equal(0xff, pixel.A); +#endif } [Fact] @@ -104,12 +101,11 @@ public async void RefreshAsyncTest() socket.Requests.Add("host:transport:169.254.109.177:5555"); socket.Requests.Add("framebuffer:"); - socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebufferheader.bin")); - socket.SyncDataReceived.Enqueue(File.ReadAllBytes("Assets/framebuffer.bin")); + socket.SyncDataReceived.Enqueue(await File.ReadAllBytesAsync("Assets/framebufferheader.bin")); + socket.SyncDataReceived.Enqueue(await File.ReadAllBytesAsync("Assets/framebuffer.bin")); - using Framebuffer framebuffer = new(device); + using Framebuffer framebuffer = new(device, (endPoint) => socket); - Factories.AdbSocketFactory = (endPoint) => socket; await framebuffer.RefreshAsync(); Assert.NotNull(framebuffer); @@ -133,21 +129,20 @@ public async void RefreshAsyncTest() Assert.Equal(1u, header.Version); Assert.Equal(0u, header.ColorSpace); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - using Bitmap image = (Bitmap)framebuffer; - Assert.NotNull(image); - Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); - - Assert.Equal(1, image.Width); - Assert.Equal(1, image.Height); - - Color pixel = image.GetPixel(0, 0); - Assert.Equal(0x35, pixel.R); - Assert.Equal(0x4a, pixel.G); - Assert.Equal(0x4c, pixel.B); - Assert.Equal(0xff, pixel.A); - } +#if WINDOWS + using Bitmap image = (Bitmap)framebuffer; + Assert.NotNull(image); + Assert.Equal(PixelFormat.Format32bppArgb, image.PixelFormat); + + Assert.Equal(1, image.Width); + Assert.Equal(1, image.Height); + + Color pixel = image.GetPixel(0, 0); + Assert.Equal(0x35, pixel.R); + Assert.Equal(0x4a, pixel.G); + Assert.Equal(0x4c, pixel.B); + Assert.Equal(0xff, pixel.A); +#endif } } } diff --git a/AdvancedSharpAdbClient.Tests/Logs/ShellStreamTests.cs b/AdvancedSharpAdbClient.Tests/Models/ShellStreamTests.cs similarity index 93% rename from AdvancedSharpAdbClient.Tests/Logs/ShellStreamTests.cs rename to AdvancedSharpAdbClient.Tests/Models/ShellStreamTests.cs index 09a47fcc..112bc28f 100644 --- a/AdvancedSharpAdbClient.Tests/Logs/ShellStreamTests.cs +++ b/AdvancedSharpAdbClient.Tests/Models/ShellStreamTests.cs @@ -1,10 +1,9 @@ using System; using System.IO; using System.Text; -using System.Threading.Tasks; using Xunit; -namespace AdvancedSharpAdbClient.Logs.Tests +namespace AdvancedSharpAdbClient.Models.Tests { public class ShellStreamTests { @@ -49,7 +48,7 @@ public void CRLFAtStartTest() stream.Position = 0; byte[] buffer = new byte[2]; - int read = shellStream.Read(buffer, 0, 2); + int read = shellStream.Read(buffer.AsSpan(0, 2)); Assert.Equal(2, read); Assert.Equal((byte)'\n', buffer[0]); Assert.Equal((byte)'H', buffer[1]); @@ -68,7 +67,7 @@ public void MultipleCRLFInStringTest() stream.Position = 0; byte[] buffer = new byte[100]; - int read = shellStream.Read(buffer, 0, 100); + int read = shellStream.Read(buffer.AsSpan(0, 100)); string actual = Encoding.ASCII.GetString(buffer, 0, read); Assert.Equal("\n1\n2\n3\n4\n5", actual); @@ -86,19 +85,19 @@ public void PendingByteTest() using MemoryStream stream = GetStream("\r\nH\ra"); using ShellStream shellStream = new(stream, false); byte[] buffer = new byte[1]; - int read = shellStream.Read(buffer, 0, 1); + int read = shellStream.Read(buffer.AsSpan(0, 1)); Assert.Equal(1, read); Assert.Equal((byte)'\n', buffer[0]); - read = shellStream.Read(buffer, 0, 1); + read = shellStream.Read(buffer.AsSpan(0, 1)); Assert.Equal(1, read); Assert.Equal((byte)'H', buffer[0]); - read = shellStream.Read(buffer, 0, 1); + read = shellStream.Read(buffer.AsSpan(0, 1)); Assert.Equal(1, read); Assert.Equal((byte)'\r', buffer[0]); - read = shellStream.Read(buffer, 0, 1); + read = shellStream.Read(buffer.AsSpan(0, 1)); Assert.Equal(1, read); Assert.Equal((byte)'a', buffer[0]); } diff --git a/AdvancedSharpAdbClient.Tests/Properties/GlobalUsings.cs b/AdvancedSharpAdbClient.Tests/Properties/GlobalUsings.cs new file mode 100644 index 00000000..bf4ebe71 --- /dev/null +++ b/AdvancedSharpAdbClient.Tests/Properties/GlobalUsings.cs @@ -0,0 +1,14 @@ +#region AdvancedSharpAdbClient +global using AdvancedSharpAdbClient.DeviceCommands; +global using AdvancedSharpAdbClient.Exceptions; +global using AdvancedSharpAdbClient.Logs; +global using AdvancedSharpAdbClient.Models; +global using AdvancedSharpAdbClient.Models.DeviceCommands; +global using AdvancedSharpAdbClient.Polyfills; +global using AdvancedSharpAdbClient.Receivers; +global using AdvancedSharpAdbClient.Receivers.DeviceCommands; +#endregion + +#region AdvancedSharpAdbClient.Tests +global using AdvancedSharpAdbClient.Tests; +#endregion \ No newline at end of file diff --git a/AdvancedSharpAdbClient.Tests/Receivers/ConsoleOutputReceiverTests.cs b/AdvancedSharpAdbClient.Tests/Receivers/ConsoleOutputReceiverTests.cs index a7e804de..46da400c 100644 --- a/AdvancedSharpAdbClient.Tests/Receivers/ConsoleOutputReceiverTests.cs +++ b/AdvancedSharpAdbClient.Tests/Receivers/ConsoleOutputReceiverTests.cs @@ -1,9 +1,8 @@ -using AdvancedSharpAdbClient.Exceptions; -using System; +using System; using System.IO; using Xunit; -namespace AdvancedSharpAdbClient.Tests +namespace AdvancedSharpAdbClient.Receivers.Tests { /// /// Tests the class. diff --git a/AdvancedSharpAdbClient.Tests/SocketBasedTests.cs b/AdvancedSharpAdbClient.Tests/SocketBasedTests.cs index f025d646..9afb513b 100644 --- a/AdvancedSharpAdbClient.Tests/SocketBasedTests.cs +++ b/AdvancedSharpAdbClient.Tests/SocketBasedTests.cs @@ -1,5 +1,4 @@ -using AdvancedSharpAdbClient.Tests; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,10 +10,10 @@ namespace AdvancedSharpAdbClient { public class SocketBasedTests { - protected AdbClient TestClient { get; private set; } - protected SocketBasedTests(bool integrationTest, bool doDispose) { + Func AdbSocketFactory; + // this.EndPoint = AdbClient.Instance.EndPoint; #if DEBUG // Use the tracing adb socket factory to run the tests on an actual device. @@ -22,45 +21,49 @@ protected SocketBasedTests(bool integrationTest, bool doDispose) if (integrationTest) { TracingAdbSocket tracingSocket = new(EndPoint) { DoDispose = doDispose }; - - Factories.AdbSocketFactory = (endPoint) => tracingSocket; + AdbSocketFactory = (endPoint) => tracingSocket; } else { DummyAdbSocket socket = new(); - Factories.AdbSocketFactory = (endPoint) => socket; + AdbSocketFactory = (endPoint) => socket; } IntegrationTest = integrationTest; #else // In release mode (e.g. on the build server), // never run integration tests. - DummyAdbSocket socket = new DummyAdbSocket(); - Factories.AdbSocketFactory = (endPoint) => socket; + DummyAdbSocket socket = new(); + AdbSocketFactory = (endPoint) => socket; IntegrationTest = false; #endif - Socket = (IDummyAdbSocket)Factories.AdbSocketFactory(EndPoint); - - TestClient = new AdbClient(); + Socket = (IDummyAdbSocket)AdbSocketFactory(EndPoint); - Factories.Reset(); + TestClient = new AdbClient(AdbSocketFactory); } - protected static AdbResponse[] NoResponses { get; } = Array.Empty(); - protected static AdbResponse[] OkResponse { get; } = new AdbResponse[] { AdbResponse.OK }; - protected static string[] NoResponseMessages { get; } = Array.Empty(); + protected static AdbResponse[] NoResponses { get; } = []; + protected static AdbResponse[] OkResponse { get; } = [AdbResponse.OK]; + protected static string[] NoResponseMessages { get; } = []; + protected static string[] NoRequests { get; } = []; + protected static (SyncCommand, string)[] NoSyncRequests { get; } = []; + protected static SyncCommand[] NoSyncResponses { get; } = []; protected static DeviceData Device { get; } = new() { Serial = "169.254.109.177:5555", State = DeviceState.Online }; + protected AdbClient TestClient { get; init; } + protected IDummyAdbSocket Socket { get; set; } public EndPoint EndPoint { get; set; } public bool IntegrationTest { get; set; } + #region Action + /// /// /// Runs an ADB helper test, either as a unit test or as an integration test. @@ -109,15 +112,15 @@ protected void RunTest( /// The messages that the ADB sever should send. /// The messages that should follow the . /// The requests the client should send. - /// The of . + /// The of which the should use. /// The test to run. protected void RunTest( IEnumerable responses, IEnumerable responseMessages, IEnumerable requests, - Stream shellStream, + IEnumerable shellStreams, Action test) => - RunTest(responses, responseMessages, requests, null, null, null, null, shellStream, test); + RunTest(responses, responseMessages, requests, null, null, null, null, shellStreams, test); /// /// @@ -188,7 +191,7 @@ protected void RunTest( /// The messages that the ADB sever should send. /// The of data which the ADB sever should send. /// The of data which the client should send. - /// The of . + /// The of which the should use. /// The test to run. protected void RunTest( IEnumerable responses, @@ -198,15 +201,13 @@ protected void RunTest( IEnumerable syncResponses, IEnumerable syncDataReceived, IEnumerable syncDataSent, - Stream shellStream, + IEnumerable shellStreams, Action test) { // If we are running unit tests, we need to mock all the responses // that are sent by the device. Do that now. if (!IntegrationTest) { - Socket.ShellStream = shellStream; - foreach (AdbResponse response in responses) { Socket.Responses.Enqueue(response); @@ -232,6 +233,14 @@ protected void RunTest( Socket.SyncDataReceived.Enqueue(syncDatum); } } + + if (shellStreams != null) + { + foreach (Stream shellStream in shellStreams) + { + Socket.ShellStreams.Enqueue(shellStream); + } + } } Exception exception = null; @@ -255,13 +264,14 @@ protected void RunTest( Assert.Empty(Socket.Responses); Assert.Empty(Socket.SyncResponses); Assert.Empty(Socket.SyncDataReceived); + Assert.Empty(Socket.ShellStreams); // Make sure a request was sent - Assert.Equal(requests.ToList(), Socket.Requests); + Assert.Equal(requests.ToArray(), Socket.Requests); if (syncRequests != null) { - Assert.Equal(syncRequests.ToList(), Socket.SyncRequests); + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); } else { @@ -270,7 +280,7 @@ protected void RunTest( if (syncDataSent != null) { - AssertEqual(syncDataSent.ToList(), Socket.SyncDataSent.ToList()); + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); } else { @@ -281,23 +291,23 @@ protected void RunTest( { // Make sure the traffic sent on the wire matches the traffic // we have defined in our unit test. - Assert.Equal(requests.ToList(), Socket.Requests); + Assert.Equal(requests.ToArray(), Socket.Requests); if (syncRequests != null) { - Assert.Equal(syncRequests.ToList(), Socket.SyncRequests); + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); } else { Assert.Empty(Socket.SyncRequests); } - Assert.Equal(responses.ToList(), Socket.Responses); - Assert.Equal(responseMessages.ToList(), Socket.ResponseMessages); + Assert.Equal(responses.ToArray(), Socket.Responses); + Assert.Equal(responseMessages.ToArray(), Socket.ResponseMessages); if (syncResponses != null) { - Assert.Equal(syncResponses.ToList(), Socket.SyncResponses); + Assert.Equal(syncResponses.ToArray(), Socket.SyncResponses); } else { @@ -306,7 +316,7 @@ protected void RunTest( if (syncDataReceived != null) { - AssertEqual(syncDataReceived.ToList(), Socket.SyncDataReceived.ToList()); + AssertEqual(syncDataReceived.ToArray(), Socket.SyncDataReceived.ToArray()); } else { @@ -315,12 +325,21 @@ protected void RunTest( if (syncDataSent != null) { - AssertEqual(syncDataSent.ToList(), Socket.SyncDataSent.ToList()); + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); } else { Assert.Empty(Socket.SyncDataSent); } + + if (shellStreams != null) + { + Assert.Equal(shellStreams.ToArray(), [.. Socket.ShellStreams]); + } + else + { + Assert.Empty(Socket.ShellStreams); + } } if (exception != null) @@ -329,9 +348,13 @@ protected void RunTest( } } + #endregion + + #region Function + /// /// - /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// Runs an ADB helper test, either as a unit test or as an integration test. /// /// /// When running as a unit test, the and @@ -350,17 +373,17 @@ protected void RunTest( /// The messages that should follow the . /// The requests the client should send. /// The test to run. - /// A which represents the asynchronous operation. - protected Task RunTestAsync( + /// The result of . + protected TResult RunTest( IEnumerable responses, IEnumerable responseMessages, IEnumerable requests, - Func test) => - RunTestAsync(responses, responseMessages, requests, null, null, null, null, null, test); + Func test) => + RunTest(responses, responseMessages, requests, null, null, null, null, null, test); /// /// - /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// Runs an ADB helper test, either as a unit test or as an integration test. /// /// /// When running as a unit test, the and @@ -378,20 +401,20 @@ protected Task RunTestAsync( /// The messages that the ADB sever should send. /// The messages that should follow the . /// The requests the client should send. - /// The of . + /// The of which the should use. /// The test to run. - /// A which represents the asynchronous operation. - protected Task RunTestAsync( + /// The result of . + protected TResult RunTest( IEnumerable responses, IEnumerable responseMessages, IEnumerable requests, - Stream shellStream, - Func test) => - RunTestAsync(responses, responseMessages, requests, null, null, null, null, shellStream, test); + IEnumerable shellStreams, + Func test) => + RunTest(responses, responseMessages, requests, null, null, null, null, shellStreams, test); /// /// - /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// Runs an ADB helper test, either as a unit test or as an integration test. /// /// /// When running as a unit test, the and @@ -414,8 +437,8 @@ protected Task RunTestAsync( /// The of data which the ADB sever should send. /// The of data which the client should send. /// The test to run. - /// A which represents the asynchronous operation. - protected Task RunTestAsync( + /// The result of . + protected TResult RunTest( IEnumerable responses, IEnumerable responseMessages, IEnumerable requests, @@ -423,8 +446,8 @@ protected Task RunTestAsync( IEnumerable syncResponses, IEnumerable syncDataReceived, IEnumerable syncDataSent, - Func test) => - RunTestAsync( + Func test) => + RunTest( responses, responseMessages, requests, @@ -437,7 +460,7 @@ protected Task RunTestAsync( /// /// - /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// Runs an ADB helper test, either as a unit test or as an integration test. /// /// /// When running as a unit test, the and @@ -459,10 +482,10 @@ protected Task RunTestAsync( /// The messages that the ADB sever should send. /// The of data which the ADB sever should send. /// The of data which the client should send. - /// The of . + /// The of which the should use. /// The test to run. - /// A which represents the asynchronous operation. - protected async Task RunTestAsync( + /// The result of . + protected TResult RunTest( IEnumerable responses, IEnumerable responseMessages, IEnumerable requests, @@ -470,15 +493,13 @@ protected async Task RunTestAsync( IEnumerable syncResponses, IEnumerable syncDataReceived, IEnumerable syncDataSent, - Stream shellStream, - Func test) + IEnumerable shellStreams, + Func test) { // If we are running unit tests, we need to mock all the responses // that are sent by the device. Do that now. if (!IntegrationTest) { - Socket.ShellStream = shellStream; - foreach (AdbResponse response in responses) { Socket.Responses.Enqueue(response); @@ -504,17 +525,22 @@ protected async Task RunTestAsync( Socket.SyncDataReceived.Enqueue(syncDatum); } } + + if (shellStreams != null) + { + foreach (Stream shellStream in shellStreams) + { + Socket.ShellStreams.Enqueue(shellStream); + } + } } + TResult result = default; Exception exception = null; try { - await test(); - } - catch (AggregateException ex) - { - exception = ex.InnerExceptions.Count == 1 ? ex.InnerException : ex; + result = test(); } catch (Exception ex) { @@ -531,13 +557,14 @@ protected async Task RunTestAsync( Assert.Empty(Socket.Responses); Assert.Empty(Socket.SyncResponses); Assert.Empty(Socket.SyncDataReceived); + Assert.Empty(Socket.ShellStreams); // Make sure a request was sent - Assert.Equal(requests.ToList(), Socket.Requests); + Assert.Equal(requests.ToArray(), Socket.Requests); if (syncRequests != null) { - Assert.Equal(syncRequests.ToList(), Socket.SyncRequests); + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); } else { @@ -546,7 +573,7 @@ protected async Task RunTestAsync( if (syncDataSent != null) { - AssertEqual(syncDataSent.ToList(), Socket.SyncDataSent.ToList()); + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); } else { @@ -557,23 +584,23 @@ protected async Task RunTestAsync( { // Make sure the traffic sent on the wire matches the traffic // we have defined in our unit test. - Assert.Equal(requests.ToList(), Socket.Requests); + Assert.Equal(requests.ToArray(), Socket.Requests); if (syncRequests != null) { - Assert.Equal(syncRequests.ToList(), Socket.SyncRequests); + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); } else { Assert.Empty(Socket.SyncRequests); } - Assert.Equal(responses.ToList(), Socket.Responses); - Assert.Equal(responseMessages.ToList(), Socket.ResponseMessages); + Assert.Equal(responses.ToArray(), Socket.Responses); + Assert.Equal(responseMessages.ToArray(), Socket.ResponseMessages); if (syncResponses != null) { - Assert.Equal(syncResponses.ToList(), Socket.SyncResponses); + Assert.Equal(syncResponses.ToArray(), Socket.SyncResponses); } else { @@ -582,7 +609,7 @@ protected async Task RunTestAsync( if (syncDataReceived != null) { - AssertEqual(syncDataReceived.ToList(), Socket.SyncDataReceived.ToList()); + AssertEqual(syncDataReceived.ToArray(), Socket.SyncDataReceived.ToArray()); } else { @@ -591,49 +618,619 @@ protected async Task RunTestAsync( if (syncDataSent != null) { - AssertEqual(syncDataSent.ToList(), Socket.SyncDataSent.ToList()); + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); } else { Assert.Empty(Socket.SyncDataSent); } - } - if (exception != null) - { - throw exception; + if (shellStreams != null) + { + Assert.Equal(shellStreams.ToArray(), [.. Socket.ShellStreams]); + } + else + { + Assert.Empty(Socket.ShellStreams); + } } + + return exception != null ? throw exception : result; } - protected static IEnumerable Requests(params string[] requests) => requests; + #endregion - protected static IEnumerable ResponseMessages(params string[] requests) => requests; + #region Task - protected static IEnumerable<(SyncCommand, string)> SyncRequests(SyncCommand command, string path) - { - yield return (command, path); - } + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The test to run. + /// A which represents the asynchronous operation. + protected Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + Func test) => + RunTestAsync(responses, responseMessages, requests, null, null, null, null, null, test); - protected static IEnumerable<(SyncCommand, string)> SyncRequests(SyncCommand command, string path, SyncCommand command2, string path2) - { - yield return (command, path); - yield return (command2, path2); - } + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The of which the should use. + /// The test to run. + /// A which represents the asynchronous operation. + protected Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + IEnumerable shellStreams, + Func test) => + RunTestAsync(responses, responseMessages, requests, null, null, null, null, shellStreams, test); - protected static IEnumerable<(SyncCommand, string)> SyncRequests(SyncCommand command, string path, SyncCommand command2, string path2, SyncCommand command3, string path3) - { - yield return (command, path); - yield return (command2, path2); - yield return (command3, path3); - } + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The requests the client should send. + /// The messages that the ADB sever should send. + /// The of data which the ADB sever should send. + /// The of data which the client should send. + /// The test to run. + /// A which represents the asynchronous operation. + protected Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + IEnumerable<(SyncCommand, string)> syncRequests, + IEnumerable syncResponses, + IEnumerable syncDataReceived, + IEnumerable syncDataSent, + Func test) => + RunTestAsync( + responses, + responseMessages, + requests, + syncRequests, + syncResponses, + syncDataReceived, + syncDataSent, + null, + test); - protected static IEnumerable OkResponses(int count) + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The requests the client should send. + /// The messages that the ADB sever should send. + /// The of data which the ADB sever should send. + /// The of data which the client should send. + /// The of which the should use. + /// The test to run. + /// A which represents the asynchronous operation. + protected async Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + IEnumerable<(SyncCommand, string)> syncRequests, + IEnumerable syncResponses, + IEnumerable syncDataReceived, + IEnumerable syncDataSent, + IEnumerable shellStreams, + Func test) { - for (int i = 0; i < count; i++) + // If we are running unit tests, we need to mock all the responses + // that are sent by the device. Do that now. + if (!IntegrationTest) + { + foreach (AdbResponse response in responses) + { + Socket.Responses.Enqueue(response); + } + + foreach (string responseMessage in responseMessages) + { + Socket.ResponseMessages.Enqueue(responseMessage); + } + + if (syncResponses != null) + { + foreach (SyncCommand syncResponse in syncResponses) + { + Socket.SyncResponses.Enqueue(syncResponse); + } + } + + if (syncDataReceived != null) + { + foreach (byte[] syncDatum in syncDataReceived) + { + Socket.SyncDataReceived.Enqueue(syncDatum); + } + } + + if (shellStreams != null) + { + foreach (Stream shellStream in shellStreams) + { + Socket.ShellStreams.Enqueue(shellStream); + } + } + } + + Exception exception = null; + + try { - yield return AdbResponse.OK; + await test(); } - } + catch (AggregateException ex) + { + exception = ex.InnerExceptions.Count == 1 ? ex.InnerException : ex; + } + catch (Exception ex) + { + exception = ex; + } + + if (!IntegrationTest) + { + // If we are running unit tests, we need to make sure all messages + // were read, and the correct request was sent. + + // Make sure the messages were read + Assert.Empty(Socket.ResponseMessages); + Assert.Empty(Socket.Responses); + Assert.Empty(Socket.SyncResponses); + Assert.Empty(Socket.SyncDataReceived); + Assert.Empty(Socket.ShellStreams); + + // Make sure a request was sent + Assert.Equal(requests.ToArray(), Socket.Requests); + + if (syncRequests != null) + { + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); + } + else + { + Assert.Empty(Socket.SyncRequests); + } + + if (syncDataSent != null) + { + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); + } + else + { + Assert.Empty(Socket.SyncDataSent); + } + } + else + { + // Make sure the traffic sent on the wire matches the traffic + // we have defined in our unit test. + Assert.Equal(requests.ToArray(), Socket.Requests); + + if (syncRequests != null) + { + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); + } + else + { + Assert.Empty(Socket.SyncRequests); + } + + Assert.Equal(responses.ToArray(), Socket.Responses); + Assert.Equal(responseMessages.ToArray(), Socket.ResponseMessages); + + if (syncResponses != null) + { + Assert.Equal(syncResponses.ToArray(), Socket.SyncResponses); + } + else + { + Assert.Empty(Socket.SyncResponses); + } + + if (syncDataReceived != null) + { + AssertEqual(syncDataReceived.ToArray(), Socket.SyncDataReceived.ToArray()); + } + else + { + Assert.Empty(Socket.SyncDataReceived); + } + + if (syncDataSent != null) + { + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); + } + else + { + Assert.Empty(Socket.SyncDataSent); + } + + if (shellStreams != null) + { + Assert.Equal(shellStreams.ToArray(), [.. Socket.ShellStreams]); + } + else + { + Assert.Empty(Socket.ShellStreams); + } + } + + if (exception != null) + { + throw exception; + } + } + + #endregion + + #region Task + + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The test to run. + /// A which return the result of . + protected Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + Func> test) => + RunTestAsync(responses, responseMessages, requests, null, null, null, null, null, test); + + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The of which the should use. + /// The test to run. + /// A which return the result of . + protected Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + IEnumerable shellStreams, + Func> test) => + RunTestAsync(responses, responseMessages, requests, null, null, null, null, shellStreams, test); + + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The requests the client should send. + /// The messages that the ADB sever should send. + /// The of data which the ADB sever should send. + /// The of data which the client should send. + /// The test to run. + /// A which return the result of . + protected Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + IEnumerable<(SyncCommand, string)> syncRequests, + IEnumerable syncResponses, + IEnumerable syncDataReceived, + IEnumerable syncDataSent, + Func> test) => + RunTestAsync( + responses, + responseMessages, + requests, + syncRequests, + syncResponses, + syncDataReceived, + syncDataSent, + null, + test); + + /// + /// + /// Runs an async ADB helper test, either as a unit test or as an integration test. + /// + /// + /// When running as a unit test, the and + /// are used by the to mock the responses an actual device + /// would send; and the parameter is used to ensure the code + /// did send the correct requests to the device. + /// + /// + /// When running as an integration test, all three parameters, , + /// and are used to validate + /// that the traffic we simulate in the unit tests matches the traffic that is actually sent + /// over the wire. + /// + /// + /// The messages that the ADB sever should send. + /// The messages that should follow the . + /// The requests the client should send. + /// The requests the client should send. + /// The messages that the ADB sever should send. + /// The of data which the ADB sever should send. + /// The of data which the client should send. + /// The of which the should use. + /// The test to run. + /// A which return the result of . + protected async Task RunTestAsync( + IEnumerable responses, + IEnumerable responseMessages, + IEnumerable requests, + IEnumerable<(SyncCommand, string)> syncRequests, + IEnumerable syncResponses, + IEnumerable syncDataReceived, + IEnumerable syncDataSent, + IEnumerable shellStreams, + Func> test) + { + // If we are running unit tests, we need to mock all the responses + // that are sent by the device. Do that now. + if (!IntegrationTest) + { + foreach (AdbResponse response in responses) + { + Socket.Responses.Enqueue(response); + } + + foreach (string responseMessage in responseMessages) + { + Socket.ResponseMessages.Enqueue(responseMessage); + } + + if (syncResponses != null) + { + foreach (SyncCommand syncResponse in syncResponses) + { + Socket.SyncResponses.Enqueue(syncResponse); + } + } + + if (syncDataReceived != null) + { + foreach (byte[] syncDatum in syncDataReceived) + { + Socket.SyncDataReceived.Enqueue(syncDatum); + } + } + + if (shellStreams != null) + { + foreach (Stream shellStream in shellStreams) + { + Socket.ShellStreams.Enqueue(shellStream); + } + } + } + + TResult result = default; + Exception exception = null; + + try + { + result = await test(); + } + catch (AggregateException ex) + { + exception = ex.InnerExceptions.Count == 1 ? ex.InnerException : ex; + } + catch (Exception ex) + { + exception = ex; + } + + if (!IntegrationTest) + { + // If we are running unit tests, we need to make sure all messages + // were read, and the correct request was sent. + + // Make sure the messages were read + Assert.Empty(Socket.ResponseMessages); + Assert.Empty(Socket.Responses); + Assert.Empty(Socket.SyncResponses); + Assert.Empty(Socket.SyncDataReceived); + Assert.Empty(Socket.ShellStreams); + + // Make sure a request was sent + Assert.Equal(requests.ToArray(), Socket.Requests); + + if (syncRequests != null) + { + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); + } + else + { + Assert.Empty(Socket.SyncRequests); + } + + if (syncDataSent != null) + { + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); + } + else + { + Assert.Empty(Socket.SyncDataSent); + } + } + else + { + // Make sure the traffic sent on the wire matches the traffic + // we have defined in our unit test. + Assert.Equal(requests.ToArray(), Socket.Requests); + + if (syncRequests != null) + { + Assert.Equal(syncRequests.ToArray(), Socket.SyncRequests); + } + else + { + Assert.Empty(Socket.SyncRequests); + } + + Assert.Equal(responses.ToArray(), Socket.Responses); + Assert.Equal(responseMessages.ToArray(), Socket.ResponseMessages); + + if (syncResponses != null) + { + Assert.Equal(syncResponses.ToArray(), Socket.SyncResponses); + } + else + { + Assert.Empty(Socket.SyncResponses); + } + + if (syncDataReceived != null) + { + AssertEqual(syncDataReceived.ToArray(), Socket.SyncDataReceived.ToArray()); + } + else + { + Assert.Empty(Socket.SyncDataReceived); + } + + if (syncDataSent != null) + { + AssertEqual(syncDataSent.ToArray(), Socket.SyncDataSent.ToArray()); + } + else + { + Assert.Empty(Socket.SyncDataSent); + } + + if (shellStreams != null) + { + Assert.Equal(shellStreams.ToArray(), [.. Socket.ShellStreams]); + } + else + { + Assert.Empty(Socket.ShellStreams); + } + } + + return exception != null ? throw exception : result; + } + + #endregion + + protected static IEnumerable OkResponses(int count) => Enumerable.Repeat(AdbResponse.OK, count); private static void AssertEqual(IList expected, IList actual) { diff --git a/AdvancedSharpAdbClient.Tests/SyncServiceTests.Async.cs b/AdvancedSharpAdbClient.Tests/SyncServiceTests.Async.cs index 5aed6b66..ad0bafe2 100644 --- a/AdvancedSharpAdbClient.Tests/SyncServiceTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/SyncServiceTests.Async.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using Xunit; @@ -9,66 +8,113 @@ namespace AdvancedSharpAdbClient.Tests { public partial class SyncServiceTests { + /// + /// Tests the method. + /// [Fact] public async void StatAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - FileStatistics value = null; - - await RunTestAsync( + FileStatistics value = await RunTestAsync( OkResponses(2), NoResponseMessages, - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests(SyncCommand.STAT, "/fstab.donatello"), - new SyncCommand[] { SyncCommand.STAT }, - new byte[][] { new byte[] { 160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0 } }, + ["host:transport:169.254.109.177:5555", "sync:"], + [(SyncCommand.STAT, "/fstab.donatello")], + [SyncCommand.STAT], + [[160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0]], null, async () => { - using SyncService service = new(Socket, device); - value = await service.StatAsync("/fstab.donatello"); + using SyncService service = new(Socket, Device); + return await service.StatAsync("/fstab.donatello"); }); - Assert.NotNull(value); - Assert.Equal(UnixFileMode.Regular, value.FileMode & UnixFileMode.TypeMask); + Assert.Equal(UnixFileType.Regular, value.FileType & UnixFileType.TypeMask); Assert.Equal(597, value.Size); - Assert.Equal(DateTimeHelper.Epoch.ToLocalTime(), value.Time); + Assert.Equal(DateTimeExtensions.Epoch.ToLocalTime(), value.Time); } + /// + /// Tests the method. + /// [Fact] public async void GetListingAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; + List value = await RunTestAsync( + OkResponses(2), + [".", "..", "sdcard0", "emulated"], + ["host:transport:169.254.109.177:5555", "sync:"], + [(SyncCommand.LIST, "/storage")], + [SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DONE], + [ + [233, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86], + [237, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86], + [255, 161, 0, 0, 24, 0, 0, 0, 152, 130, 56, 86], + [109, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86] + ], + null, + async () => + { + using SyncService service = new(Socket, Device); + return await service.GetDirectoryListingAsync("/storage"); + }); - List value = null; + Assert.Equal(4, value.Count); - await RunTestAsync( + DateTime time = new DateTime(2015, 11, 3, 9, 47, 4, DateTimeKind.Utc).ToLocalTime(); + + FileStatistics dir = value[0]; + Assert.Equal(".", dir.Path); + Assert.Equal((UnixFileType)16873, dir.FileType); + Assert.Equal(0, dir.Size); + Assert.Equal(time, dir.Time); + + FileStatistics parentDir = value[1]; + Assert.Equal("..", parentDir.Path); + Assert.Equal((UnixFileType)16877, parentDir.FileType); + Assert.Equal(0, parentDir.Size); + Assert.Equal(time, parentDir.Time); + + FileStatistics sdcard0 = value[2]; + Assert.Equal("sdcard0", sdcard0.Path); + Assert.Equal((UnixFileType)41471, sdcard0.FileType); + Assert.Equal(24, sdcard0.Size); + Assert.Equal(time, sdcard0.Time); + + FileStatistics emulated = value[3]; + Assert.Equal("emulated", emulated.Path); + Assert.Equal((UnixFileType)16749, emulated.FileType); + Assert.Equal(0, emulated.Size); + Assert.Equal(time, emulated.Time); + } + + /// + /// Tests the method. + /// + [Fact] + public async void GetAsyncListingTest() + { + List value = await RunTestAsync( OkResponses(2), - ResponseMessages(".", "..", "sdcard0", "emulated"), - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests(SyncCommand.LIST, "/storage"), - new SyncCommand[] { SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DONE }, - new byte[][] - { - new byte[] { 233, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86 }, - new byte[] { 237, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86 }, - new byte[] { 255, 161, 0, 0, 24, 0, 0, 0, 152, 130, 56, 86 }, - new byte[] { 109, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86 } - }, + [".", "..", "sdcard0", "emulated"], + ["host:transport:169.254.109.177:5555", "sync:"], + [(SyncCommand.LIST, "/storage")], + [SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DONE], + [ + [233, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86], + [237, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86], + [255, 161, 0, 0, 24, 0, 0, 0, 152, 130, 56, 86], + [109, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86] + ], null, async () => { - using SyncService service = new(Socket, device); - value = (await service.GetDirectoryListingAsync("/storage")).ToList(); + List value = []; + using SyncService service = new(Socket, Device); + await foreach (FileStatistics statistics in service.GetDirectoryAsyncListing("/storage")) + { + value.Add(statistics); + } + return value; }); Assert.Equal(4, value.Count); @@ -77,98 +123,94 @@ await RunTestAsync( FileStatistics dir = value[0]; Assert.Equal(".", dir.Path); - Assert.Equal((UnixFileMode)16873, dir.FileMode); + Assert.Equal((UnixFileType)16873, dir.FileType); Assert.Equal(0, dir.Size); Assert.Equal(time, dir.Time); FileStatistics parentDir = value[1]; Assert.Equal("..", parentDir.Path); - Assert.Equal((UnixFileMode)16877, parentDir.FileMode); + Assert.Equal((UnixFileType)16877, parentDir.FileType); Assert.Equal(0, parentDir.Size); Assert.Equal(time, parentDir.Time); FileStatistics sdcard0 = value[2]; Assert.Equal("sdcard0", sdcard0.Path); - Assert.Equal((UnixFileMode)41471, sdcard0.FileMode); + Assert.Equal((UnixFileType)41471, sdcard0.FileType); Assert.Equal(24, sdcard0.Size); Assert.Equal(time, sdcard0.Time); FileStatistics emulated = value[3]; Assert.Equal("emulated", emulated.Path); - Assert.Equal((UnixFileMode)16749, emulated.FileMode); + Assert.Equal((UnixFileType)16749, emulated.FileType); Assert.Equal(0, emulated.Size); Assert.Equal(time, emulated.Time); } + /// + /// Tests the method. + /// [Fact] public async void PullAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - await using MemoryStream stream = new(); - byte[] content = File.ReadAllBytes("Assets/fstab.bin"); + byte[] content = await File.ReadAllBytesAsync("Assets/fstab.bin"); byte[] contentLength = BitConverter.GetBytes(content.Length); await RunTestAsync( OkResponses(2), - ResponseMessages(), - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests(SyncCommand.STAT, "/fstab.donatello").Union(SyncRequests(SyncCommand.RECV, "/fstab.donatello")), - new SyncCommand[] { SyncCommand.STAT, SyncCommand.DATA, SyncCommand.DONE }, - new byte[][] - { - new byte[] { 160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0 }, + NoResponseMessages, + ["host:transport:169.254.109.177:5555", "sync:"], + [ + (SyncCommand.STAT, "/fstab.donatello"), + (SyncCommand.RECV, "/fstab.donatello") + ], + [SyncCommand.STAT, SyncCommand.DATA, SyncCommand.DONE], + [ + [160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0], contentLength, content - }, + ], null, async () => { - using SyncService service = new(Socket, device); - await service.PullAsync("/fstab.donatello", stream, null, CancellationToken.None); + using SyncService service = new(Socket, Device); + await service.PullAsync("/fstab.donatello", stream, null); }); // Make sure the data that has been sent to the stream is the expected data Assert.Equal(content, stream.ToArray()); } + /// + /// Tests the method. + /// [Fact] public async void PushAsyncTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - Stream stream = File.OpenRead("Assets/fstab.bin"); - byte[] content = File.ReadAllBytes("Assets/fstab.bin"); - List contentMessage = new(); - contentMessage.AddRange(SyncCommandConverter.GetBytes(SyncCommand.DATA)); - contentMessage.AddRange(BitConverter.GetBytes(content.Length)); - contentMessage.AddRange(content); + FileStream stream = File.OpenRead("Assets/fstab.bin"); + byte[] content = await File.ReadAllBytesAsync("Assets/fstab.bin"); + byte[] contentMessage = + [ + .. SyncCommandConverter.GetBytes(SyncCommand.DATA), + .. BitConverter.GetBytes(content.Length), + .. content, + ]; await RunTestAsync( OkResponses(2), - ResponseMessages(), - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests( - SyncCommand.SEND, "/sdcard/test,644", - SyncCommand.DONE, "1446505200"), - new SyncCommand[] { SyncCommand.OKAY }, + NoResponseMessages, + ["host:transport:169.254.109.177:5555", "sync:"], + [ + (SyncCommand.SEND, "/sdcard/test,644"), + (SyncCommand.DONE, "1446505200") + ], + [SyncCommand.OKAY], null, - new byte[][] - { - contentMessage.ToArray() - }, + [contentMessage], async () => { - using SyncService service = new(Socket, device); - await service.PushAsync(stream, "/sdcard/test", 0644, new DateTime(2015, 11, 2, 23, 0, 0, DateTimeKind.Utc), null, CancellationToken.None); + using SyncService service = new(Socket, Device); + await service.PushAsync(stream, "/sdcard/test", 0644, new DateTime(2015, 11, 2, 23, 0, 0, DateTimeKind.Utc), null); }); } } diff --git a/AdvancedSharpAdbClient.Tests/SyncServiceTests.cs b/AdvancedSharpAdbClient.Tests/SyncServiceTests.cs index 42589feb..cff3b5fa 100644 --- a/AdvancedSharpAdbClient.Tests/SyncServiceTests.cs +++ b/AdvancedSharpAdbClient.Tests/SyncServiceTests.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using Xunit; namespace AdvancedSharpAdbClient.Tests @@ -20,166 +18,150 @@ public SyncServiceTests() : base(integrationTest: false, doDispose: false) { } + /// + /// Tests the method. + /// [Fact] public void StatTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - FileStatistics value = null; - - RunTest( + FileStatistics value = RunTest( OkResponses(2), NoResponseMessages, - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests(SyncCommand.STAT, "/fstab.donatello"), - new SyncCommand[] { SyncCommand.STAT }, - new byte[][] { new byte[] { 160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0 } }, + ["host:transport:169.254.109.177:5555", "sync:"], + [(SyncCommand.STAT, "/fstab.donatello")], + [SyncCommand.STAT], + [[160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0]], null, () => { - using SyncService service = new(Socket, device); - value = service.Stat("/fstab.donatello"); + using SyncService service = new(Socket, Device); + return service.Stat("/fstab.donatello"); }); - Assert.NotNull(value); - Assert.Equal(UnixFileMode.Regular, value.FileMode & UnixFileMode.TypeMask); + Assert.Equal(UnixFileType.Regular, value.FileType & UnixFileType.TypeMask); Assert.Equal(597, value.Size); - Assert.Equal(DateTimeHelper.Epoch.ToLocalTime(), value.Time); + Assert.Equal(DateTimeExtensions.Epoch.ToLocalTime(), value.Time); } + /// + /// Tests the method. + /// [Fact] public void GetListingTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - List value = null; - - RunTest( + FileStatistics[] value = RunTest( OkResponses(2), - ResponseMessages(".", "..", "sdcard0", "emulated"), - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests(SyncCommand.LIST, "/storage"), - new SyncCommand[] { SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DONE }, - new byte[][] - { - new byte[] { 233, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86 }, - new byte[] { 237, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86 }, - new byte[] { 255, 161, 0, 0, 24, 0, 0, 0, 152, 130, 56, 86 }, - new byte[] { 109, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86 } - }, + [".", "..", "sdcard0", "emulated"], + ["host:transport:169.254.109.177:5555", "sync:"], + [(SyncCommand.LIST, "/storage")], + [SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DENT, SyncCommand.DONE], + [ + [233, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86], + [237, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86], + [255, 161, 0, 0, 24, 0, 0, 0, 152, 130, 56, 86], + [109, 65, 0, 0, 0, 0, 0, 0, 152, 130, 56, 86] + ], null, () => { - using SyncService service = new(Socket, device); - value = service.GetDirectoryListing("/storage").ToList(); + using SyncService service = new(Socket, Device); + return service.GetDirectoryListing("/storage").ToArray(); }); - Assert.Equal(4, value.Count); + Assert.Equal(4, value.Length); DateTime time = new DateTime(2015, 11, 3, 9, 47, 4, DateTimeKind.Utc).ToLocalTime(); FileStatistics dir = value[0]; Assert.Equal(".", dir.Path); - Assert.Equal((UnixFileMode)16873, dir.FileMode); + Assert.Equal((UnixFileType)16873, dir.FileType); Assert.Equal(0, dir.Size); Assert.Equal(time, dir.Time); FileStatistics parentDir = value[1]; Assert.Equal("..", parentDir.Path); - Assert.Equal((UnixFileMode)16877, parentDir.FileMode); + Assert.Equal((UnixFileType)16877, parentDir.FileType); Assert.Equal(0, parentDir.Size); Assert.Equal(time, parentDir.Time); FileStatistics sdcard0 = value[2]; Assert.Equal("sdcard0", sdcard0.Path); - Assert.Equal((UnixFileMode)41471, sdcard0.FileMode); + Assert.Equal((UnixFileType)41471, sdcard0.FileType); Assert.Equal(24, sdcard0.Size); Assert.Equal(time, sdcard0.Time); FileStatistics emulated = value[3]; Assert.Equal("emulated", emulated.Path); - Assert.Equal((UnixFileMode)16749, emulated.FileMode); + Assert.Equal((UnixFileType)16749, emulated.FileType); Assert.Equal(0, emulated.Size); Assert.Equal(time, emulated.Time); } + /// + /// Tests the method. + /// [Fact] public void PullTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - using MemoryStream stream = new(); byte[] content = File.ReadAllBytes("Assets/fstab.bin"); byte[] contentLength = BitConverter.GetBytes(content.Length); RunTest( OkResponses(2), - ResponseMessages(), - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests(SyncCommand.STAT, "/fstab.donatello").Union(SyncRequests(SyncCommand.RECV, "/fstab.donatello")), - new SyncCommand[] { SyncCommand.STAT, SyncCommand.DATA, SyncCommand.DONE }, - new byte[][] - { - new byte[] { 160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0 }, + NoResponseMessages, + ["host:transport:169.254.109.177:5555", "sync:"], + [ + (SyncCommand.STAT, "/fstab.donatello"), + (SyncCommand.RECV, "/fstab.donatello") + ], + [SyncCommand.STAT, SyncCommand.DATA, SyncCommand.DONE], + [ + [160, 129, 0, 0, 85, 2, 0, 0, 0, 0, 0, 0], contentLength, content - }, + ], null, () => { - using SyncService service = new(Socket, device); - service.Pull("/fstab.donatello", stream, null, CancellationToken.None); + using SyncService service = new(Socket, Device); + service.Pull("/fstab.donatello", stream); }); // Make sure the data that has been sent to the stream is the expected data Assert.Equal(content, stream.ToArray()); } + /// + /// Tests the method. + /// [Fact] public void PushTest() { - DeviceData device = new() - { - Serial = "169.254.109.177:5555", - State = DeviceState.Online - }; - - Stream stream = File.OpenRead("Assets/fstab.bin"); + FileStream stream = File.OpenRead("Assets/fstab.bin"); byte[] content = File.ReadAllBytes("Assets/fstab.bin"); - List contentMessage = new(); - contentMessage.AddRange(SyncCommandConverter.GetBytes(SyncCommand.DATA)); - contentMessage.AddRange(BitConverter.GetBytes(content.Length)); - contentMessage.AddRange(content); + byte[] contentMessage = + [ + .. SyncCommandConverter.GetBytes(SyncCommand.DATA), + .. BitConverter.GetBytes(content.Length), + .. content, + ]; RunTest( OkResponses(2), - ResponseMessages(), - Requests("host:transport:169.254.109.177:5555", "sync:"), - SyncRequests( - SyncCommand.SEND, "/sdcard/test,644", - SyncCommand.DONE, "1446505200"), - new SyncCommand[] { SyncCommand.OKAY }, + NoResponseMessages, + ["host:transport:169.254.109.177:5555", "sync:"], + [ + (SyncCommand.SEND, "/sdcard/test,644"), + (SyncCommand.DONE, "1446505200") + ], + [SyncCommand.OKAY], null, - new byte[][] - { - contentMessage.ToArray() - }, + [contentMessage], () => { - using SyncService service = new(Socket, device); - service.Push(stream, "/sdcard/test", 0644, new DateTime(2015, 11, 2, 23, 0, 0, DateTimeKind.Utc), null, CancellationToken.None); + using SyncService service = new(Socket, Device); + service.Push(stream, "/sdcard/test", 0644, new DateTime(2015, 11, 2, 23, 0, 0, DateTimeKind.Utc)); }); } } diff --git a/AdvancedSharpAdbClient.Tests/TcpSocketTests.Async.cs b/AdvancedSharpAdbClient.Tests/TcpSocketTests.Async.cs index e33f62cf..2b91f159 100644 --- a/AdvancedSharpAdbClient.Tests/TcpSocketTests.Async.cs +++ b/AdvancedSharpAdbClient.Tests/TcpSocketTests.Async.cs @@ -1,6 +1,8 @@ -using System.Net; +using System; +using System.Net; using System.Net.Sockets; using System.Text; +using System.Threading; using Xunit; namespace AdvancedSharpAdbClient.Tests @@ -10,20 +12,66 @@ public partial class TcpSocketTests [Fact] public async void LifecycleAsyncTest() { - TcpSocket socket = new(); + using TcpSocket socket = new(); Assert.False(socket.Connected); - socket.Connect(new DnsEndPoint("www.bing.com", 80)); + await socket.ConnectAsync(new DnsEndPoint("www.bing.com", 80)); Assert.True(socket.Connected); byte[] data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\n\n"); - await socket.SendAsync(data, 0, data.Length, SocketFlags.None); + await socket.SendAsync(data, data.Length, SocketFlags.None); byte[] responseData = new byte[128]; - await socket.ReceiveAsync(responseData, 0, responseData.Length, SocketFlags.None); + await socket.ReceiveAsync(responseData, responseData.Length, SocketFlags.None); _ = Encoding.ASCII.GetString(responseData); + } + + [Fact] + public async void LifecycleAsyncMemoryTest() + { + using TcpSocket socket = new(); + Assert.False(socket.Connected); + + await socket.ConnectAsync(new DnsEndPoint("www.bing.com", 80)); + Assert.True(socket.Connected); + + ReadOnlyMemory data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\n\n"); + await socket.SendAsync(data, SocketFlags.None); + + byte[] responseData = new byte[128]; + await socket.ReceiveAsync(responseData.AsMemory(), SocketFlags.None); + + _ = Encoding.ASCII.GetString(responseData); + } + + /// + /// Tests the method. + /// + [Fact] + public async void ReconnectAsyncTest() + { + using TcpSocket socket = new(); + Assert.False(socket.Connected); + + await socket.ConnectAsync(new DnsEndPoint("www.bing.com", 80)); + Assert.True(socket.Connected); + socket.Dispose(); + Assert.False(socket.Connected); + + await socket.ReconnectAsync(); + Assert.True(socket.Connected); + } + + /// + /// Tests the method. + /// + [Fact] + public async void CreateUnsupportedSocketAsyncTest() + { + using TcpSocket socket = new(); + _ = await Assert.ThrowsAsync(() => socket.ConnectAsync(new CustomEndPoint()).AsTask()); } } } diff --git a/AdvancedSharpAdbClient.Tests/TcpSocketTests.cs b/AdvancedSharpAdbClient.Tests/TcpSocketTests.cs index 4dcd5b9f..4c6a77de 100644 --- a/AdvancedSharpAdbClient.Tests/TcpSocketTests.cs +++ b/AdvancedSharpAdbClient.Tests/TcpSocketTests.cs @@ -15,26 +15,46 @@ public partial class TcpSocketTests [Fact] public void LifecycleTest() { - TcpSocket socket = new(); + using TcpSocket socket = new(); Assert.False(socket.Connected); socket.Connect(new DnsEndPoint("www.bing.com", 80)); Assert.True(socket.Connected); byte[] data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\n\n"); - socket.Send(data, 0, data.Length, SocketFlags.None); + socket.Send(data, data.Length, SocketFlags.None); byte[] responseData = new byte[128]; - socket.Receive(responseData, 0, SocketFlags.None); + socket.Receive(responseData, responseData.Length, SocketFlags.None); _ = Encoding.ASCII.GetString(responseData); - socket.Dispose(); } + [Fact] + public void LifecycleSpanTest() + { + using TcpSocket socket = new(); + Assert.False(socket.Connected); + + socket.Connect(new DnsEndPoint("www.bing.com", 80)); + Assert.True(socket.Connected); + + ReadOnlySpan data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\n\n"); + socket.Send(data, SocketFlags.None); + + byte[] responseData = new byte[128]; + socket.Receive([.. responseData], SocketFlags.None); + + _ = Encoding.ASCII.GetString(responseData); + } + + /// + /// Tests the method. + /// [Fact] public void ReconnectTest() { - TcpSocket socket = new(); + using TcpSocket socket = new(); Assert.False(socket.Connected); socket.Connect(new DnsEndPoint("www.bing.com", 80)); @@ -47,22 +67,27 @@ public void ReconnectTest() Assert.True(socket.Connected); } + /// + /// Tests the property. + /// [Fact] public void BufferSizeTest() { - TcpSocket socket = new() + using TcpSocket socket = new() { ReceiveBufferSize = 1024 }; // https://stackoverflow.com/questions/29356626/is-there-a-way-to-reduce-the-minimum-lower-limit-of-the-socket-send-buffer-size Assert.Equal(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? 2304 : 1024, socket.ReceiveBufferSize); - socket.Dispose(); } + /// + /// Tests the method. + /// [Fact] public void CreateUnsupportedSocketTest() { - TcpSocket socket = new(); + using TcpSocket socket = new(); _ = Assert.Throws(() => socket.Connect(new CustomEndPoint())); } } diff --git a/AdvancedSharpAdbClient/AdbClient.Async.cs b/AdvancedSharpAdbClient/AdbClient.Async.cs index c3c6bbe9..c15d9a52 100644 --- a/AdvancedSharpAdbClient/AdbClient.Async.cs +++ b/AdvancedSharpAdbClient/AdbClient.Async.cs @@ -3,8 +3,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; using System; using System.Collections.Generic; using System.Diagnostics; @@ -26,9 +24,10 @@ public partial class AdbClient public async Task GetAdbVersionAsync(CancellationToken cancellationToken = default) { using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync("host:version", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); - string version = await socket.ReadStringAsync(cancellationToken); + + await socket.SendAdbRequestAsync("host:version", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string version = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); return int.Parse(version, NumberStyles.HexNumber); } @@ -37,7 +36,7 @@ public async Task GetAdbVersionAsync(CancellationToken cancellationToken = public async Task KillAdbAsync(CancellationToken cancellationToken = default) { using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync("host:kill", cancellationToken); + await socket.SendAdbRequestAsync("host:kill", cancellationToken).ConfigureAwait(false); // The host will immediately close the connection after the kill // command has been sent; no need to read the response. @@ -47,13 +46,13 @@ public async Task KillAdbAsync(CancellationToken cancellationToken = default) public async Task> GetDevicesAsync(CancellationToken cancellationToken = default) { using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync("host:devices-l", cancellationToken); - await socket.ReadAdbResponseAsync(cancellationToken); - string reply = await socket.ReadStringAsync(cancellationToken); - string[] data = reply.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + await socket.SendAdbRequestAsync("host:devices-l", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string reply = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); - return data.Select(DeviceData.CreateFromAdbData); + string[] data = reply.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return data.Select(x => new DeviceData(x)); } /// @@ -64,33 +63,29 @@ public async Task CreateForwardAsync(DeviceData device, string local, strin using IAdbSocket socket = adbSocketFactory(EndPoint); string rebind = allowRebind ? string.Empty : "norebind:"; - await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:forward:{rebind}{local};{remote}", cancellationToken); - _ = await socket.ReadAdbResponseAsync(cancellationToken); - _ = await socket.ReadAdbResponseAsync(cancellationToken); - string portString = await socket.ReadStringAsync(cancellationToken); + await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:forward:{rebind}{local};{remote}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string portString = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); return portString != null && int.TryParse(portString, out int port) ? port : 0; } - /// - public Task CreateForwardAsync(DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind, CancellationToken cancellationToken = default) => - CreateForwardAsync(device, local?.ToString(), remote?.ToString(), allowRebind, cancellationToken); - /// public async Task CreateReverseForwardAsync(DeviceData device, string remote, string local, bool allowRebind, CancellationToken cancellationToken = default) { EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); string rebind = allowRebind ? string.Empty : "norebind:"; - await socket.SendAdbRequestAsync($"reverse:forward:{rebind}{remote};{local}", cancellationToken); - _ = await socket.ReadAdbResponseAsync(cancellationToken); - _ = await socket.ReadAdbResponseAsync(cancellationToken); - string portString = await socket.ReadStringAsync(cancellationToken); + await socket.SendAdbRequestAsync($"reverse:forward:{rebind}{remote};{local}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string portString = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); return portString != null && int.TryParse(portString, out int port) ? port : 0; } @@ -100,10 +95,10 @@ public async Task RemoveReverseForwardAsync(DeviceData device, string remote, Ca EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync($"reverse:killforward:{remote}", cancellationToken); - AdbResponse response = socket.ReadAdbResponse(); + await socket.SendAdbRequestAsync($"reverse:killforward:{remote}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -112,10 +107,10 @@ public async Task RemoveAllReverseForwardsAsync(DeviceData device, CancellationT EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync($"reverse:killforward-all", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync($"reverse:killforward-all", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -124,8 +119,8 @@ public async Task RemoveForwardAsync(DeviceData device, int localPort, Cancellat EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:killforward:tcp:{localPort}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:killforward:tcp:{localPort}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -134,8 +129,8 @@ public async Task RemoveAllForwardsAsync(DeviceData device, CancellationToken ca EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:killforward-all", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:killforward-all", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -144,14 +139,12 @@ public async Task> ListForwardAsync(DeviceData device, EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:list-forward", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); - - string data = await socket.ReadStringAsync(cancellationToken); + await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:list-forward", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string data = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); - string[] parts = data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - - return parts.Select(ForwardData.FromString); + string[] parts = data.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return parts.Select(x => ForwardData.FromString(x)); } /// @@ -160,36 +153,78 @@ public async Task> ListReverseForwardAsync(DeviceData d EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync($"reverse:list-forward", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync($"reverse:list-forward", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string data = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); - string data = await socket.ReadStringAsync(cancellationToken); - - string[] parts = data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + string[] parts = data.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return parts.Select(x => ForwardData.FromString(x)); + } - return parts.Select(ForwardData.FromString); + /// + public async Task ExecuteServerCommandAsync(string target, string command, Encoding encoding, CancellationToken cancellationToken = default) + { + ExceptionExtensions.ThrowIfNull(encoding); + using IAdbSocket socket = adbSocketFactory(EndPoint); + await ExecuteServerCommandAsync(target, command, socket, encoding, cancellationToken); } /// - public Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, CancellationToken cancellationToken = default) => - ExecuteRemoteCommandAsync(command, device, receiver, Encoding, cancellationToken); + public async Task ExecuteServerCommandAsync(string target, string command, IAdbSocket socket, Encoding encoding, CancellationToken cancellationToken = default) + { + ExceptionExtensions.ThrowIfNull(encoding); + + StringBuilder request = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = request.AppendFormat("{0}:", target); + } + _ = request.Append(command); + + await socket.SendAdbRequestAsync(request.ToString(), cancellationToken); + await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + } /// - public async Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) + public async Task ExecuteRemoteCommandAsync(string command, DeviceData device, Encoding encoding, CancellationToken cancellationToken = default) { EnsureDevice(device); + ExceptionExtensions.ThrowIfNull(encoding); using IAdbSocket socket = adbSocketFactory(EndPoint); - cancellationToken.Register(socket.Dispose); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:{command}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + + await ExecuteServerCommandAsync("shell", command, socket, encoding, cancellationToken); + } + + /// + public async Task ExecuteServerCommandAsync(string target, string command, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) + { + ExceptionExtensions.ThrowIfNull(encoding); + using IAdbSocket socket = adbSocketFactory(EndPoint); + await ExecuteServerCommandAsync(target, command, socket, receiver, encoding, cancellationToken); + } + + /// + public async Task ExecuteServerCommandAsync(string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) + { + ExceptionExtensions.ThrowIfNull(encoding); + + StringBuilder request = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = request.AppendFormat("{0}:", target); + } + _ = request.Append(command); + + await socket.SendAdbRequestAsync(request.ToString(), cancellationToken); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); try { + cancellationToken.Register(socket.Dispose); using StreamReader reader = new(socket.GetShellStream(), encoding); // Previously, we would loop while reader.Peek() >= 0. Turns out that this would // break too soon in certain cases (about every 10 loops, so it appears to be a timing @@ -197,7 +232,7 @@ public async Task ExecuteRemoteCommandAsync(string command, DeviceData device, I // -- one of the integration test fetches output 1000 times and found no truncations. while (!cancellationToken.IsCancellationRequested) { - string line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + string? line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line == null) { break; } @@ -220,6 +255,18 @@ public async Task ExecuteRemoteCommandAsync(string command, DeviceData device, I } } + /// + public async Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken = default) + { + EnsureDevice(device); + ExceptionExtensions.ThrowIfNull(encoding); + + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SetDeviceAsync(device, cancellationToken); + + await ExecuteServerCommandAsync("shell", command, socket, receiver, encoding, cancellationToken); + } + /// public async Task GetFrameBufferAsync(DeviceData device, CancellationToken cancellationToken = default) { @@ -232,42 +279,36 @@ public async Task GetFrameBufferAsync(DeviceData device, Cancellati return framebuffer; } - /// - public Task RunLogServiceAsync(DeviceData device, Action messageSink, params LogId[] logNames) => - RunLogServiceAsync(device, messageSink, default, logNames); - /// public async Task RunLogServiceAsync(DeviceData device, Action messageSink, CancellationToken cancellationToken, params LogId[] logNames) { - ExceptionExtensions.ThrowIfNull(messageSink); - EnsureDevice(device); + ExceptionExtensions.ThrowIfNull(messageSink); // The 'log' service has been deprecated, see // https://android.googlesource.com/platform/system/core/+/7aa39a7b199bb9803d3fd47246ee9530b4a96177 using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - StringBuilder request = new(); - request.Append("shell:logcat -B"); + StringBuilder request = new StringBuilder().Append("shell:logcat -B"); foreach (LogId logName in logNames) { - request.Append($" -b {logName.ToString().ToLowerInvariant()}"); + _ = request.AppendFormat(" -b {0}", logName.ToString().ToLowerInvariant()); } - await socket.SendAdbRequestAsync(request.ToString(), cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync(request.ToString(), cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); #if NETCOREAPP3_0_OR_GREATER await #endif - using Stream stream = socket.GetShellStream(); + using Stream stream = socket.GetShellStream(); LogReader reader = new(stream); while (!cancellationToken.IsCancellationRequested) { - LogEntry entry = null; + LogEntry? entry = null; try { @@ -294,12 +335,11 @@ public async Task RebootAsync(string into, DeviceData device, CancellationToken { EnsureDevice(device); - string request = $"reboot:{into}"; - using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync(request, cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"reboot:{into}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -308,10 +348,10 @@ public async Task PairAsync(DnsEndPoint endpoint, string code, Cancellat ExceptionExtensions.ThrowIfNull(endpoint); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host:pair:{code}:{endpoint.Host}:{endpoint.Port}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); - string results = await socket.ReadStringAsync(cancellationToken); - return results; + await socket.SendAdbRequestAsync($"host:pair:{code}:{endpoint.Host}:{endpoint.Port}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + + return await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); } /// @@ -320,10 +360,10 @@ public async Task ConnectAsync(DnsEndPoint endpoint, CancellationToken c ExceptionExtensions.ThrowIfNull(endpoint); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host:connect:{endpoint.Host}:{endpoint.Port}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); - string results = await socket.ReadStringAsync(cancellationToken); - return results; + await socket.SendAdbRequestAsync($"host:connect:{endpoint.Host}:{endpoint.Port}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + + return await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); } /// @@ -332,10 +372,10 @@ public async Task DisconnectAsync(DnsEndPoint endpoint, CancellationToke ExceptionExtensions.ThrowIfNull(endpoint); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host:disconnect:{endpoint.Host}:{endpoint.Port}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); - string results = await socket.ReadStringAsync(cancellationToken); - return results; + await socket.SendAdbRequestAsync($"host:disconnect:{endpoint.Host}:{endpoint.Port}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + + return await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); } /// @@ -356,23 +396,25 @@ protected async Task RootAsync(string request, DeviceData device, CancellationTo EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync(request, cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync(request, cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); // ADB will send some additional data byte[] buffer = new byte[1024]; - int read = socket.Read(buffer); + int read = await socket.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - string responseMessage = Encoding.UTF8.GetString(buffer, 0, read); + string responseMessage = +#if HAS_BUFFERS + Encoding.UTF8.GetString(buffer.AsSpan(0, read)); +#else + Encoding.UTF8.GetString(buffer, 0, read); +#endif // see https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/daemon/restart_service.cpp // for possible return strings -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (!responseMessage.Contains("restarting", StringComparison.OrdinalIgnoreCase)) -#else - if (responseMessage.IndexOf("restarting", StringComparison.OrdinalIgnoreCase) == -1) -#endif { throw new AdbException(responseMessage); } @@ -380,18 +422,14 @@ protected async Task RootAsync(string request, DeviceData device, CancellationTo { // Give adbd some time to kill itself and come back up. // We can't use wait-for-device because devices (e.g. adb over network) might not come back. - Utilities.Delay(3000, cancellationToken).GetAwaiter().GetResult(); + await Extensions.Delay(3000, cancellationToken).ConfigureAwait(false); } } - /// - public Task InstallAsync(DeviceData device, Stream apk, params string[] arguments) => InstallAsync(device, apk, default, arguments); - /// public async Task InstallAsync(DeviceData device, Stream apk, CancellationToken cancellationToken, params string[] arguments) { EnsureDevice(device); - ExceptionExtensions.ThrowIfNull(apk); if (!apk.CanRead || !apk.CanSeek) @@ -399,14 +437,13 @@ public async Task InstallAsync(DeviceData device, Stream apk, CancellationToken throw new ArgumentOutOfRangeException(nameof(apk), "The apk stream must be a readable and seekable stream"); } - StringBuilder requestBuilder = new(); - _ = requestBuilder.Append("exec:cmd package 'install'"); + StringBuilder requestBuilder = new StringBuilder().Append("exec:cmd package 'install'"); if (arguments != null) { foreach (string argument in arguments) { - _ = requestBuilder.Append($" {argument}"); + _ = requestBuilder.AppendFormat(" {0}", argument); } } @@ -415,27 +452,30 @@ public async Task InstallAsync(DeviceData device, Stream apk, CancellationToken _ = requestBuilder.Append($" -S {apk.Length}"); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); byte[] buffer = new byte[32 * 1024]; int read = 0; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - while ((read = await apk.ReadAsync(buffer, cancellationToken)) > 0) -#elif !NET35 - while ((read = await apk.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + while ((read = await apk.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) + { +#if HAS_BUFFERS + await socket.SendAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); #else - while ((read = apk.Read(buffer, 0, buffer.Length)) > 0) + await socket.SendAsync(buffer, read, cancellationToken).ConfigureAwait(false); #endif - { - await socket.SendAsync(buffer, read, cancellationToken); } - read = await socket.ReadAsync(buffer, cancellationToken); - string value = Encoding.UTF8.GetString(buffer, 0, read); + read = await socket.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + string value = +#if HAS_BUFFERS + Encoding.UTF8.GetString(buffer.AsSpan(0, read)); +#else + Encoding.UTF8.GetString(buffer, 0, read); +#endif if (!value.Contains("Success")) { @@ -443,21 +483,17 @@ public async Task InstallAsync(DeviceData device, Stream apk, CancellationToken } } - /// - public Task InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments) => - InstallMultipleAsync(device, splitAPKs, packageName, default, arguments); - /// public async Task InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, CancellationToken cancellationToken, params string[] arguments) { EnsureDevice(device); - + ExceptionExtensions.ThrowIfNull(splitAPKs); ExceptionExtensions.ThrowIfNull(packageName); - string session = await InstallCreateAsync(device, packageName, cancellationToken, arguments); + string session = await InstallCreateAsync(device, packageName, cancellationToken, arguments).ConfigureAwait(false); int i = 0; - IEnumerable tasks = splitAPKs.Select(async (splitAPK) => + await Extensions.WhenAll(splitAPKs.Select(async splitAPK => { if (splitAPK == null || !splitAPK.CanRead || !splitAPK.CanSeek) { @@ -467,43 +503,35 @@ public async Task InstallMultipleAsync(DeviceData device, IEnumerable sp try { - await InstallWriteAsync(device, splitAPK, $"{nameof(splitAPK)}{i++}", session, cancellationToken); + await InstallWriteAsync(device, splitAPK, $"{nameof(splitAPK)}{i++}", session, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Debug.WriteLine(ex.Message); } - }); - foreach (Task task in tasks) - { - await task; - } + })).ConfigureAwait(false); - await InstallCommitAsync(device, session, cancellationToken); + await InstallCommitAsync(device, session, cancellationToken).ConfigureAwait(false); } - /// - public Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments) => - InstallMultipleAsync(device, baseAPK, splitAPKs, default, arguments); - /// public async Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, CancellationToken cancellationToken, params string[] arguments) { EnsureDevice(device); - ExceptionExtensions.ThrowIfNull(baseAPK); + ExceptionExtensions.ThrowIfNull(splitAPKs); if (!baseAPK.CanRead || !baseAPK.CanSeek) { throw new ArgumentOutOfRangeException(nameof(baseAPK), "The apk stream must be a readable and seekable stream"); } - string session = await InstallCreateAsync(device, null, cancellationToken, arguments); + string session = await InstallCreateAsync(device, null, cancellationToken, arguments).ConfigureAwait(false); - await InstallWriteAsync(device, baseAPK, nameof(baseAPK), session, cancellationToken); + await InstallWriteAsync(device, baseAPK, nameof(baseAPK), session, cancellationToken).ConfigureAwait(false); int i = 0; - IEnumerable tasks = splitAPKs.Select(async (splitAPK) => + await Extensions.WhenAll(splitAPKs.Select(async splitAPK => { if (splitAPK == null || !splitAPK.CanRead || !splitAPK.CanSeek) { @@ -513,59 +541,54 @@ public async Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnume try { - await InstallWriteAsync(device, splitAPK, $"{nameof(splitAPK)}{i++}", session, cancellationToken); + await InstallWriteAsync(device, splitAPK, $"{nameof(splitAPK)}{i++}", session, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Debug.WriteLine(ex.Message); } - }); - foreach (Task task in tasks) - { - await task; - } + })).ConfigureAwait(false); - await InstallCommitAsync(device, session, cancellationToken); + await InstallCommitAsync(device, session, cancellationToken).ConfigureAwait(false); } /// - public Task InstallCreateAsync(DeviceData device, string packageName = null, params string[] arguments) => - InstallCreateAsync(device, packageName, default, arguments); - - - /// - public async Task InstallCreateAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments) + public async Task InstallCreateAsync(DeviceData device, string? packageName, CancellationToken cancellationToken, params string[] arguments) { EnsureDevice(device); - StringBuilder requestBuilder = new(); - _ = requestBuilder.Append("exec:cmd package 'install-create'"); - _ = requestBuilder.Append(packageName.IsNullOrWhiteSpace() ? string.Empty : $" -p {packageName}"); + StringBuilder requestBuilder = new StringBuilder().Append("exec:cmd package 'install-create'"); + + if (!StringExtensions.IsNullOrWhiteSpace(packageName)) + { + requestBuilder.AppendFormat(" -p {0}", packageName); + } if (arguments != null) { foreach (string argument in arguments) { - _ = requestBuilder.Append($" {argument}"); + _ = requestBuilder.AppendFormat(" {0}", argument); } } using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + string result = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) + ?? throw new AdbException($"The {nameof(result)} of {nameof(InstallCreateAsync)} is null."); if (!result.Contains("Success")) { - throw new AdbException(await reader.ReadToEndAsync(cancellationToken)); + throw new AdbException(await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)); } - int arr = result.IndexOf("]") - 1 - result.IndexOf("["); - string session = result.Substring(result.IndexOf("[") + 1, arr); + int arr = result.IndexOf(']') - 1 - result.IndexOf('['); + string session = result.Substring(result.IndexOf('[') + 1, arr); return session; } @@ -573,49 +596,47 @@ public async Task InstallCreateAsync(DeviceData device, string packageNa public async Task InstallWriteAsync(DeviceData device, Stream apk, string apkName, string session, CancellationToken cancellationToken = default) { EnsureDevice(device); - ExceptionExtensions.ThrowIfNull(apk); + ExceptionExtensions.ThrowIfNull(apkName); + ExceptionExtensions.ThrowIfNull(session); if (!apk.CanRead || !apk.CanSeek) { throw new ArgumentOutOfRangeException(nameof(apk), "The apk stream must be a readable and seekable stream"); } - ExceptionExtensions.ThrowIfNull(session); - - ExceptionExtensions.ThrowIfNull(apkName); - - StringBuilder requestBuilder = new(); - requestBuilder.Append($"exec:cmd package 'install-write'"); - - // add size parameter [required for streaming installs] - // do last to override any user specified value - requestBuilder.Append($" -S {apk.Length}"); - - requestBuilder.Append($" {session} {apkName}.apk"); + StringBuilder requestBuilder = + new StringBuilder().Append($"exec:cmd package 'install-write'") + // add size parameter [required for streaming installs] + // do last to override any user specified value + .AppendFormat(" -S {0}", apk.Length) + .AppendFormat(" {0} {1}.apk", session, apkName); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); byte[] buffer = new byte[32 * 1024]; int read = 0; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - while ((read = await apk.ReadAsync(buffer, cancellationToken)) > 0) -#elif !NET35 - while ((read = await apk.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + while ((read = await apk.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) + { +#if HAS_BUFFERS + await socket.SendAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); #else - while ((read = apk.Read(buffer, 0, buffer.Length)) > 0) + await socket.SendAsync(buffer, read, cancellationToken).ConfigureAwait(false); #endif - { - await socket.SendAsync(buffer, read, cancellationToken); } - read = await socket.ReadAsync(buffer, cancellationToken); - string value = Encoding.UTF8.GetString(buffer, 0, read); + read = await socket.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + string value = +#if HAS_BUFFERS + Encoding.UTF8.GetString(buffer.AsSpan(0, read)); +#else + Encoding.UTF8.GetString(buffer, 0, read); +#endif if (!value.Contains("Success")) { @@ -627,29 +648,61 @@ public async Task InstallWriteAsync(DeviceData device, Stream apk, string apkNam public async Task InstallCommitAsync(DeviceData device, string session, CancellationToken cancellationToken = default) { using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - await socket.SendAdbRequestAsync($"exec:cmd package 'install-commit' {session}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SendAdbRequestAsync($"exec:cmd package 'install-commit' {session}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (!result.Contains("Success")) + string? result = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (result?.Contains("Success") != true) { - throw new AdbException(await reader.ReadToEndAsync(cancellationToken)); + throw new AdbException(await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)); } } /// - public async Task> GetFeatureSetAsync(DeviceData device, CancellationToken cancellationToken = default) + public async Task UninstallAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments) { + EnsureDevice(device); + + StringBuilder requestBuilder = new StringBuilder().Append("exec:cmd package 'uninstall'"); + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + _ = requestBuilder.AppendFormat(" {0}", packageName); + using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:features", cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync(requestBuilder.ToString(), cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + + using StreamReader reader = new(socket.GetShellStream(), Encoding); + string? result = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (result?.Contains("Success") != true) + { + throw new AdbException(await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)); + } + } + + /// + public async Task> GetFeatureSetAsync(DeviceData device, CancellationToken cancellationToken = default) + { + EnsureDevice(device); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); - string features = await socket.ReadStringAsync(cancellationToken); + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SendAdbRequestAsync($"host-serial:{device.Serial}:features", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string features = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); - IEnumerable featureList = features.Trim().Split(new char[] { '\n', ',' }); + IEnumerable featureList = features.Trim().Split('\n', ','); return featureList; } @@ -657,32 +710,35 @@ public async Task> GetFeatureSetAsync(DeviceData device, Can public async Task DumpScreenStringAsync(DeviceData device, CancellationToken cancellationToken = default) { EnsureDevice(device); + using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync("shell:uiautomator dump /dev/tty", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync("shell:uiautomator dump /dev/tty", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string xmlString = reader.ReadToEnd() - .Replace("Events injected: 1\r\n", string.Empty) - .Replace("UI hierchary dumped to: /dev/tty", string.Empty) - .Trim(); + string xmlString = + reader.ReadToEnd() + .Replace("Events injected: 1\r\n", string.Empty) + .Replace("UI hierchary dumped to: /dev/tty", string.Empty) + .Trim(); + if (string.IsNullOrEmpty(xmlString) || xmlString.StartsWith(" - public async Task DumpScreenAsync(DeviceData device, CancellationToken cancellationToken = default) + public async Task DumpScreenAsync(DeviceData device, CancellationToken cancellationToken = default) { + EnsureDevice(device); + string xmlString = await DumpScreenStringAsync(device, cancellationToken).ConfigureAwait(false); XmlDocument doc = new(); - string xmlString = await DumpScreenStringAsync(device, cancellationToken); if (!string.IsNullOrEmpty(xmlString)) { doc.LoadXml(xmlString); @@ -693,10 +749,11 @@ public async Task DumpScreenAsync(DeviceData device, CancellationTo #if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER /// - public async Task DumpScreenWinRTAsync(DeviceData device, CancellationToken cancellationToken = default) + public async Task DumpScreenWinRTAsync(DeviceData device, CancellationToken cancellationToken = default) { + EnsureDevice(device); + string xmlString = await DumpScreenStringAsync(device, cancellationToken).ConfigureAwait(false); Windows.Data.Xml.Dom.XmlDocument doc = new(); - string xmlString = await DumpScreenStringAsync(device, cancellationToken); if (!string.IsNullOrEmpty(xmlString)) { doc.LoadXml(xmlString); @@ -707,21 +764,24 @@ public async Task DumpScreenAsync(DeviceData device, CancellationTo #endif /// - public async Task ClickAsync(DeviceData device, Cords cords, CancellationToken cancellationToken = default) + public async Task ClickAsync(DeviceData device, Point cords, CancellationToken cancellationToken = default) { EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:input tap {cords.X} {cords.Y}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:input tap {cords.X} {cords.Y}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = (await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)).TrimStart(); + string result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart()).ConfigureAwait(false); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } @@ -733,16 +793,19 @@ public async Task ClickAsync(DeviceData device, int x, int y, CancellationToken EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:input tap {x} {y}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:input tap {x} {y}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = (await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)).TrimStart(); + string result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart()).ConfigureAwait(false); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } @@ -754,16 +817,19 @@ public async Task SwipeAsync(DeviceData device, Element first, Element second, l EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:input swipe {first.Cords.X} {first.Cords.Y} {second.Cords.X} {second.Cords.Y} {speed}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:input swipe {first.Center.X} {first.Center.Y} {second.Center.X} {second.Center.Y} {speed}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = (await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)).TrimStart(); + string result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart()).ConfigureAwait(false); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } @@ -775,79 +841,100 @@ public async Task SwipeAsync(DeviceData device, int x1, int y1, int x2, int y2, EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:input swipe {x1} {y1} {x2} {y2} {speed}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:input swipe {x1} {y1} {x2} {y2} {speed}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = (await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)).TrimStart(); + string result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart()).ConfigureAwait(false); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } } /// - public async Task IsCurrentAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) + public async Task IsAppRunningAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) { - ConsoleOutputReceiver receiver = new(); - await ExecuteRemoteCommandAsync($"dumpsys activity activities | grep mResumedActivity", device, receiver, cancellationToken); - string response = receiver.ToString().Trim(); - return response.ToString().Contains(packageName); + EnsureDevice(device); + + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:pidof {packageName}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + + using StreamReader reader = new(socket.GetShellStream(), Encoding); + string? result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart().Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()).ConfigureAwait(false); + bool intParsed = int.TryParse(result, out int pid); + return intParsed && pid > 0; } /// - public async Task IsAppRunningAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) + public async Task IsAppInForegroundAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) { - ConsoleOutputReceiver receiver = new(); - await ExecuteRemoteCommandAsync($"pidof {packageName}", device, receiver, cancellationToken); - string response = receiver.ToString().Trim(); - bool intParsed = int.TryParse(response, out int pid); - return intParsed && pid > 0; + EnsureDevice(device); + + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:dumpsys activity activities | grep mResumedActivity", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + + using StreamReader reader = new(socket.GetShellStream(), Encoding); + string result = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + return result.Contains(packageName); } /// public async Task GetAppStatusAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) { // Check if the app is in foreground - bool currentApp = await IsCurrentAppAsync(device, packageName, cancellationToken); + bool currentApp = await IsAppInForegroundAsync(device, packageName, cancellationToken).ConfigureAwait(false); if (currentApp) { return AppStatus.Foreground; } // Check if the app is running in background - bool isAppRunning = await IsAppRunningAsync(device, packageName, cancellationToken); + bool isAppRunning = await IsAppRunningAsync(device, packageName, cancellationToken).ConfigureAwait(false); return isAppRunning ? AppStatus.Background : AppStatus.Stopped; } /// - public async Task FindElementAsync(DeviceData device, string xpath = "hierarchy/node", CancellationToken cancellationToken = default) + public async Task FindElementAsync(DeviceData device, string xpath = "hierarchy/node", CancellationToken cancellationToken = default) { try { while (!cancellationToken.IsCancellationRequested) { - XmlDocument doc = await DumpScreenAsync(device, cancellationToken); - if (doc != null) + try { - XmlNode xmlNode = doc.SelectSingleNode(xpath); - if (xmlNode != null) + XmlDocument? doc = await DumpScreenAsync(device, cancellationToken).ConfigureAwait(false); + if (doc != null) { - Element element = Element.FromXmlNode(this, device, xmlNode); - if (element != null) + XmlNode? xmlNode = doc.SelectSingleNode(xpath); + if (xmlNode != null) { - return element; + Element? element = Element.FromXmlNode(this, device, xmlNode); + if (element != null) + { + return element; + } } } } - if (cancellationToken == default) + catch (XmlException) { - break; + // Ignore XmlException and try again } + if (cancellationToken == default) { break; } } } catch (Exception e) @@ -864,34 +951,40 @@ public async Task FindElementAsync(DeviceData device, string xpath = "h } /// - public async Task> FindElementsAsync(DeviceData device, string xpath = "hierarchy/node", CancellationToken cancellationToken = default) + public async Task> FindElementsAsync(DeviceData device, string xpath = "hierarchy/node", CancellationToken cancellationToken = default) { try { while (!cancellationToken.IsCancellationRequested) { - XmlDocument doc = await DumpScreenAsync(device, cancellationToken); - if (doc != null) + try { - XmlNodeList xmlNodes = doc.SelectNodes(xpath); - if (xmlNodes != null) + XmlDocument? doc = await DumpScreenAsync(device, cancellationToken).ConfigureAwait(false); + if (doc != null) { - List elements = new(xmlNodes.Count); - for (int i = 0; i < xmlNodes.Count; i++) + XmlNodeList? xmlNodes = doc.SelectNodes(xpath); + if (xmlNodes != null) { - Element element = Element.FromXmlNode(this, device, xmlNodes[i]); - if (element != null) + static IEnumerable FindElements(IAdbClient client, DeviceData device, XmlNodeList xmlNodes) { - elements.Add(element); + for (int i = 0; i < xmlNodes.Count; i++) + { + Element? element = Element.FromXmlNode(client, device, xmlNodes[i]); + if (element != null) + { + yield return element; + } + } } + return FindElements(this, device, xmlNodes); } - return elements.Count == 0 ? null : elements; } } - if (cancellationToken == default) + catch (XmlException) { - break; + // Ignore XmlException and try again } + if (cancellationToken == default) { break; } } } catch (Exception e) @@ -904,7 +997,7 @@ public async Task> FindElementsAsync(DeviceData device, string xpa throw new ShellCommandUnresponsiveException(e); } } - return null; + return Enumerable.Empty(); } #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER @@ -913,32 +1006,45 @@ public async IAsyncEnumerable FindAsyncElements(DeviceData device, stri { while (!cancellationToken.IsCancellationRequested) { - XmlDocument doc = await DumpScreenAsync(device, cancellationToken); + XmlDocument? doc = null; + + try + { + doc = await DumpScreenAsync(device, cancellationToken).ConfigureAwait(false); + } + catch (XmlException) + { + // Ignore XmlException and try again + } + catch (Exception e) + { + // If a cancellation was requested, this main loop is interrupted with an exception + // because the socket is closed. In that case, we don't need to throw a ShellCommandUnresponsiveException. + // In all other cases, something went wrong, and we want to report it to the user. + if (!cancellationToken.IsCancellationRequested) + { + throw new ShellCommandUnresponsiveException(e); + } + } + if (doc != null) { - XmlNodeList xmlNodes = doc.SelectNodes(xpath); + XmlNodeList? xmlNodes = doc.SelectNodes(xpath); if (xmlNodes != null) { - bool isBreak = false; for (int i = 0; i < xmlNodes.Count; i++) { - Element element = Element.FromXmlNode(this, device, xmlNodes[i]); + Element? element = Element.FromXmlNode(this, device, xmlNodes[i]); if (element != null) { - isBreak = true; yield return element; } } - if (isBreak) - { - break; - } + break; } } - if (cancellationToken == default) - { - break; - } + + if (cancellationToken == default) { break; } } } #endif @@ -949,16 +1055,19 @@ public async Task SendKeyEventAsync(DeviceData device, string key, CancellationT EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:input keyevent {key}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:input keyevent {key}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = (await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)).TrimStart(); + string result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart()).ConfigureAwait(false); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new InvalidKeyEventException("KeyEvent is invalid"); } @@ -970,41 +1079,47 @@ public async Task SendTextAsync(DeviceData device, string text, CancellationToke EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - await socket.SetDeviceAsync(device, cancellationToken); - await socket.SendAdbRequestAsync($"shell:input text {text}", cancellationToken); - AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:input text {text}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = (await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false)).TrimStart(); + string result = await reader.ReadToEndAsync(cancellationToken).ContinueWith(x => x.Result.TrimStart()).ConfigureAwait(false); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new InvalidTextException(); } } /// - public async Task ClearInputAsync(DeviceData device, int charCount, CancellationToken cancellationToken = default) + public async Task StartAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) { - await SendKeyEventAsync(device, "KEYCODE_MOVE_END", cancellationToken); - await ExecuteRemoteCommandAsync("input keyevent " + Utilities.Join(" ", Enumerable.Repeat("KEYCODE_DEL ", charCount)), device, null, cancellationToken); - } + EnsureDevice(device); - /// - public async Task StartAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) => - await ExecuteRemoteCommandAsync($"monkey -p {packageName} 1", device, null, cancellationToken); + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); - /// - public async Task StopAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) => - await ExecuteRemoteCommandAsync($"am force-stop {packageName}", device, null, cancellationToken); + await socket.SendAdbRequestAsync($"shell:monkey -p {packageName} 1", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + } /// - public Task BackBtnAsync(DeviceData device) => SendKeyEventAsync(device, "KEYCODE_BACK"); + public async Task StopAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken = default) + { + EnsureDevice(device); - /// - public Task HomeBtnAsync(DeviceData device) => SendKeyEventAsync(device, "KEYCODE_HOME"); + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SetDeviceAsync(device, cancellationToken).ConfigureAwait(false); + + await socket.SendAdbRequestAsync($"shell:am force-stop {packageName}", cancellationToken).ConfigureAwait(false); + _ = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + } } } #endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/AdbClient.cs b/AdvancedSharpAdbClient/AdbClient.cs index 60b18d81..59ebd5d0 100644 --- a/AdvancedSharpAdbClient/AdbClient.cs +++ b/AdvancedSharpAdbClient/AdbClient.cs @@ -2,19 +2,19 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using AdvancedSharpAdbClient.Logs; using System.IO; using System.Linq; using System.Net; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Xml; -using System.Text.RegularExpressions; namespace AdvancedSharpAdbClient { @@ -23,7 +23,7 @@ namespace AdvancedSharpAdbClient /// adb server and devices that are connected to that adb server. /// For example, to fetch a list of all devices that are currently connected to this PC, you can /// call the method. - /// To run a command on a device, you can use the + /// To run a command on a device, you can use the /// method. /// /// @@ -33,6 +33,8 @@ namespace AdvancedSharpAdbClient /// public partial class AdbClient : IAdbClient { + private static readonly char[] separator = Extensions.NewLineSeparator; + /// /// The default port to use when connecting to a device over TCP/IP. /// @@ -47,6 +49,7 @@ public partial class AdbClient : IAdbClient /// Gets a new instance of the class. /// [Obsolete("This function has been removed since SharpAdbClient. Here is a placeholder which function is gets a new instance instead of gets or sets the default instance.")] + [EditorBrowsable(EditorBrowsableState.Never)] public static IAdbClient Instance => new AdbClient(); /// @@ -57,7 +60,8 @@ public partial class AdbClient : IAdbClient /// /// Initializes a new instance of the class. /// - public AdbClient() : this(new IPEndPoint(IPAddress.Loopback, AdbServerPort), Factories.AdbSocketFactory) + public AdbClient() + : this(new IPEndPoint(IPAddress.Loopback, AdbServerPort), Factories.AdbSocketFactory) { } @@ -65,7 +69,8 @@ public AdbClient() : this(new IPEndPoint(IPAddress.Loopback, AdbServerPort), Fac /// Initializes a new instance of the class. /// /// The at which the adb server is listening. - public AdbClient(EndPoint endPoint) : this(endPoint, Factories.AdbSocketFactory) + public AdbClient(EndPoint endPoint) + : this(endPoint, Factories.AdbSocketFactory) { } @@ -74,51 +79,51 @@ public AdbClient(EndPoint endPoint) : this(endPoint, Factories.AdbSocketFactory) /// /// The host address at which the adb server is listening. /// The port at which the adb server is listening. - public AdbClient(string host, int port) : this(host, port, Factories.AdbSocketFactory) + public AdbClient(string host, int port) + : this(Extensions.CreateDnsEndPoint(host, port), Factories.AdbSocketFactory) { } /// /// Initializes a new instance of the class. /// - /// The host address at which the adb server is listening. - /// The port at which the adb server is listening. + /// The at which the adb server is listening. /// The to create . - public AdbClient(string host, int port, Func adbSocketFactory) + public AdbClient(EndPoint endPoint, Func adbSocketFactory) { - if (string.IsNullOrEmpty(host)) + ExceptionExtensions.ThrowIfNull(endPoint); + + if (endPoint is not (IPEndPoint or DnsEndPoint)) { - throw new ArgumentNullException(nameof(host)); + throw new NotSupportedException("Only TCP endpoints are supported"); } - string[] values = host.Split(':'); - - EndPoint = values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port); + EndPoint = endPoint; this.adbSocketFactory = adbSocketFactory ?? throw new ArgumentNullException(nameof(adbSocketFactory)); } /// /// Initializes a new instance of the class. /// - /// The at which the adb server is listening. + /// The host address at which the adb server is listening. + /// The port at which the adb server is listening. /// The to create . - public AdbClient(EndPoint endPoint, Func adbSocketFactory) + public AdbClient(string host, int port, Func adbSocketFactory) + : this(Extensions.CreateDnsEndPoint(host, port), adbSocketFactory) { - ExceptionExtensions.ThrowIfNull(endPoint); - - if (endPoint is not (IPEndPoint or DnsEndPoint)) - { - throw new NotSupportedException("Only TCP endpoints are supported"); - } + } - EndPoint = endPoint; - this.adbSocketFactory = adbSocketFactory ?? throw new ArgumentNullException(nameof(adbSocketFactory)); + /// + /// Initializes a new instance of the class. + /// + /// The to create . + public AdbClient(Func adbSocketFactory) + : this(new IPEndPoint(IPAddress.Loopback, AdbServerPort), adbSocketFactory) + { } /// - /// Get or set default encoding + /// Get or set default encoding. /// public static Encoding Encoding { get; set; } = Encoding.UTF8; @@ -168,8 +173,9 @@ public static byte[] CreateAdbForwardRequest(string address, int port) public int GetAdbVersion() { using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SendAdbRequest("host:version"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); string version = socket.ReadString(); return int.Parse(version, NumberStyles.HexNumber); @@ -189,13 +195,13 @@ public void KillAdb() public IEnumerable GetDevices() { using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SendAdbRequest("host:devices-l"); - socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); string reply = socket.ReadString(); - string[] data = reply.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - - return data.Select(DeviceData.CreateFromAdbData); + string[] data = reply.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return data.Select(x => new DeviceData(x)); } /// @@ -209,30 +215,26 @@ public int CreateForward(DeviceData device, string local, string remote, bool al socket.SendAdbRequest($"host-serial:{device.Serial}:forward:{rebind}{local};{remote}"); _ = socket.ReadAdbResponse(); _ = socket.ReadAdbResponse(); - string portString = socket.ReadString(); + string portString = socket.ReadString(); return portString != null && int.TryParse(portString, out int port) ? port : 0; } - /// - public int CreateForward(DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind) => - CreateForward(device, local?.ToString(), remote?.ToString(), allowRebind); - /// public int CreateReverseForward(DeviceData device, string remote, string local, bool allowRebind) { EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); - socket.SetDevice(device); + socket.SetDevice(device); string rebind = allowRebind ? string.Empty : "norebind:"; socket.SendAdbRequest($"reverse:forward:{rebind}{remote};{local}"); _ = socket.ReadAdbResponse(); _ = socket.ReadAdbResponse(); - string portString = socket.ReadString(); + string portString = socket.ReadString(); return portString != null && int.TryParse(portString, out int port) ? port : 0; } @@ -245,7 +247,7 @@ public void RemoveReverseForward(DeviceData device, string remote) socket.SetDevice(device); socket.SendAdbRequest($"reverse:killforward:{remote}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); } /// @@ -257,7 +259,7 @@ public void RemoveAllReverseForwards(DeviceData device) socket.SetDevice(device); socket.SendAdbRequest($"reverse:killforward-all"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); } /// @@ -267,7 +269,7 @@ public void RemoveForward(DeviceData device, int localPort) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host-serial:{device.Serial}:killforward:tcp:{localPort}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); } /// @@ -277,7 +279,7 @@ public void RemoveAllForwards(DeviceData device) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host-serial:{device.Serial}:killforward-all"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); } /// @@ -287,13 +289,11 @@ public IEnumerable ListForward(DeviceData device) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host-serial:{device.Serial}:list-forward"); - AdbResponse response = socket.ReadAdbResponse(); - + _ = socket.ReadAdbResponse(); string data = socket.ReadString(); - string[] parts = data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - - return parts.Select(ForwardData.FromString); + string[] parts = data.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return parts.Select(x => ForwardData.FromString(x)); } /// @@ -305,29 +305,71 @@ public IEnumerable ListReverseForward(DeviceData device) socket.SetDevice(device); socket.SendAdbRequest($"reverse:list-forward"); - AdbResponse response = socket.ReadAdbResponse(); - + _ = socket.ReadAdbResponse(); string data = socket.ReadString(); - string[] parts = data.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + string[] parts = data.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return parts.Select(x => ForwardData.FromString(x)); + } - return parts.Select(ForwardData.FromString); + /// + public void ExecuteServerCommand(string target, string command, Encoding encoding) + { + ExceptionExtensions.ThrowIfNull(encoding); + using IAdbSocket socket = adbSocketFactory(EndPoint); + ExecuteServerCommand(target, command, socket, encoding); } /// - public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver) => - ExecuteRemoteCommand(command, device, receiver, Encoding); + public void ExecuteServerCommand(string target, string command, IAdbSocket socket, Encoding encoding) + { + ExceptionExtensions.ThrowIfNull(encoding); + + StringBuilder request = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = request.AppendFormat("{0}:", target); + } + _ = request.Append(command); + + socket.SendAdbRequest(request.ToString()); + _ = socket.ReadAdbResponse(); + } /// - public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding) + public void ExecuteRemoteCommand(string command, DeviceData device, Encoding encoding) { EnsureDevice(device); + ExceptionExtensions.ThrowIfNull(encoding); using IAdbSocket socket = adbSocketFactory(EndPoint); - socket.SetDevice(device); - socket.SendAdbRequest($"shell:{command}"); - AdbResponse response = socket.ReadAdbResponse(); + + ExecuteServerCommand("shell", command, socket, encoding); + } + + /// + public void ExecuteServerCommand(string target, string command, IShellOutputReceiver receiver, Encoding encoding) + { + ExceptionExtensions.ThrowIfNull(encoding); + using IAdbSocket socket = adbSocketFactory(EndPoint); + ExecuteServerCommand(target, command, socket, receiver, encoding); + } + + /// + public void ExecuteServerCommand(string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, Encoding encoding) + { + ExceptionExtensions.ThrowIfNull(encoding); + + StringBuilder request = new(); + if (!StringExtensions.IsNullOrWhiteSpace(target)) + { + _ = request.AppendFormat("{0}:", target); + } + _ = request.Append(command); + + socket.SendAdbRequest(request.ToString()); + _ = socket.ReadAdbResponse(); try { @@ -338,10 +380,8 @@ public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutput // -- one of the integration test fetches output 1000 times and found no truncations. while (true) { - string line = reader.ReadLine(); - + string? line = reader.ReadLine(); if (line == null) { break; } - receiver?.AddOutput(line); } } @@ -356,11 +396,21 @@ public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutput } /// - public Framebuffer CreateRefreshableFramebuffer(DeviceData device) + public void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding) { EnsureDevice(device); - return new Framebuffer(device, this); + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SetDevice(device); + + ExecuteServerCommand("shell", command, socket, receiver, encoding); + } + + /// + public Framebuffer CreateRefreshableFramebuffer(DeviceData device) + { + EnsureDevice(device); + return new Framebuffer(device, this, adbSocketFactory); } /// @@ -378,32 +428,30 @@ public Framebuffer GetFrameBuffer(DeviceData device) /// public void RunLogService(DeviceData device, Action messageSink, params LogId[] logNames) { - ExceptionExtensions.ThrowIfNull(messageSink); - EnsureDevice(device); + ExceptionExtensions.ThrowIfNull(messageSink); // The 'log' service has been deprecated, see // https://android.googlesource.com/platform/system/core/+/7aa39a7b199bb9803d3fd47246ee9530b4a96177 using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); - StringBuilder request = new(); - request.Append("shell:logcat -B"); + StringBuilder request = new StringBuilder().Append("shell:logcat -B"); foreach (LogId logName in logNames) { - request.Append($" -b {logName.ToString().ToLowerInvariant()}"); + _ = request.AppendFormat(" -b {0}", logName.ToString().ToLowerInvariant()); } socket.SendAdbRequest(request.ToString()); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); using Stream stream = socket.GetShellStream(); LogReader reader = new(stream); while (true) { - LogEntry entry = null; + LogEntry? entry = null; try { @@ -430,12 +478,11 @@ public void Reboot(string into, DeviceData device) { EnsureDevice(device); - string request = $"reboot:{into}"; - using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); - socket.SendAdbRequest(request); - AdbResponse response = socket.ReadAdbResponse(); + + socket.SendAdbRequest($"reboot:{into}"); + _ = socket.ReadAdbResponse(); } /// @@ -445,9 +492,9 @@ public string Pair(DnsEndPoint endpoint, string code) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host:pair:{code}:{endpoint.Host}:{endpoint.Port}"); - AdbResponse response = socket.ReadAdbResponse(); - string results = socket.ReadString(); - return results; + _ = socket.ReadAdbResponse(); + + return socket.ReadString(); } /// @@ -457,9 +504,9 @@ public string Connect(DnsEndPoint endpoint) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host:connect:{endpoint.Host}:{endpoint.Port}"); - AdbResponse response = socket.ReadAdbResponse(); - string results = socket.ReadString(); - return results; + _ = socket.ReadAdbResponse(); + + return socket.ReadString(); } /// @@ -469,9 +516,9 @@ public string Disconnect(DnsEndPoint endpoint) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host:disconnect:{endpoint.Host}:{endpoint.Port}"); - AdbResponse response = socket.ReadAdbResponse(); - string results = socket.ReadString(); - return results; + _ = socket.ReadAdbResponse(); + + return socket.ReadString(); } /// @@ -492,21 +539,22 @@ protected void Root(string request, DeviceData device) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); socket.SendAdbRequest(request); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); // ADB will send some additional data byte[] buffer = new byte[1024]; int read = socket.Read(buffer); - string responseMessage = Encoding.UTF8.GetString(buffer, 0, read); + string responseMessage = +#if HAS_BUFFERS + Encoding.UTF8.GetString(buffer.AsSpan(0, read)); +#else + Encoding.UTF8.GetString(buffer, 0, read); +#endif // see https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/daemon/restart_service.cpp // for possible return strings -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (!responseMessage.Contains("restarting", StringComparison.OrdinalIgnoreCase)) -#else - if (responseMessage.IndexOf("restarting", StringComparison.OrdinalIgnoreCase) == -1) -#endif { throw new AdbException(responseMessage); } @@ -517,7 +565,7 @@ protected void Root(string request, DeviceData device) #if HAS_PROCESS && !WINDOWS_UWP Thread.Sleep(3000); #else - Utilities.Delay(3000).GetAwaiter().GetResult(); + Extensions.Delay(3000).Wait(); #endif } } @@ -526,7 +574,6 @@ protected void Root(string request, DeviceData device) public void Install(DeviceData device, Stream apk, params string[] arguments) { EnsureDevice(device); - ExceptionExtensions.ThrowIfNull(apk); if (!apk.CanRead || !apk.CanSeek) @@ -534,37 +581,45 @@ public void Install(DeviceData device, Stream apk, params string[] arguments) throw new ArgumentOutOfRangeException(nameof(apk), "The apk stream must be a readable and seekable stream"); } - StringBuilder requestBuilder = new(); - _ = requestBuilder.Append("exec:cmd package 'install'"); + StringBuilder requestBuilder = new StringBuilder().Append("exec:cmd package 'install'"); if (arguments != null) { foreach (string argument in arguments) { - _ = requestBuilder.Append($" {argument}"); + _ = requestBuilder.AppendFormat(" {0}", argument); } } // add size parameter [required for streaming installs] // do last to override any user specified value - _ = requestBuilder.Append($" -S {apk.Length}"); + _ = requestBuilder.AppendFormat(" -S {0}", apk.Length); using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); socket.SendAdbRequest(requestBuilder.ToString()); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); byte[] buffer = new byte[32 * 1024]; int read = 0; - while ((read = apk.Read(buffer, 0, buffer.Length)) > 0) + while ((read = apk.Read(buffer)) > 0) { +#if HAS_BUFFERS + socket.Send(buffer.AsSpan(0, read)); +#else socket.Send(buffer, read); +#endif } read = socket.Read(buffer); - string value = Encoding.UTF8.GetString(buffer, 0, read); + string value = +#if HAS_BUFFERS + Encoding.UTF8.GetString(buffer.AsSpan(0, read)); +#else + Encoding.UTF8.GetString(buffer, 0, read); +#endif if (!value.Contains("Success")) { @@ -576,7 +631,7 @@ public void Install(DeviceData device, Stream apk, params string[] arguments) public void InstallMultiple(DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments) { EnsureDevice(device); - + ExceptionExtensions.ThrowIfNull(splitAPKs); ExceptionExtensions.ThrowIfNull(packageName); string session = InstallCreate(device, packageName, arguments); @@ -607,8 +662,8 @@ public void InstallMultiple(DeviceData device, IEnumerable splitAPKs, st public void InstallMultiple(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments) { EnsureDevice(device); - ExceptionExtensions.ThrowIfNull(baseAPK); + ExceptionExtensions.ThrowIfNull(splitAPKs); if (!baseAPK.CanRead || !baseAPK.CanSeek) { @@ -642,19 +697,22 @@ public void InstallMultiple(DeviceData device, Stream baseAPK, IEnumerable - public string InstallCreate(DeviceData device, string packageName = null, params string[] arguments) + public string InstallCreate(DeviceData device, string? packageName = null, params string[] arguments) { EnsureDevice(device); - StringBuilder requestBuilder = new(); - _ = requestBuilder.Append("exec:cmd package 'install-create'"); - _ = requestBuilder.Append(packageName.IsNullOrWhiteSpace() ? string.Empty : $" -p {packageName}"); + StringBuilder requestBuilder = new StringBuilder().Append("exec:cmd package 'install-create'"); + + if (!StringExtensions.IsNullOrWhiteSpace(packageName)) + { + requestBuilder.AppendFormat(" -p {0}", packageName); + } if (arguments != null) { foreach (string argument in arguments) { - _ = requestBuilder.Append($" {argument}"); + _ = requestBuilder.AppendFormat(" {0}", argument); } } @@ -662,18 +720,18 @@ public string InstallCreate(DeviceData device, string packageName = null, params socket.SetDevice(device); socket.SendAdbRequest(requestBuilder.ToString()); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = reader.ReadLine(); + string result = reader.ReadLine() ?? throw new AdbException($"The {nameof(result)} of {nameof(InstallCreate)} is null."); if (!result.Contains("Success")) { throw new AdbException(reader.ReadToEnd()); } - int arr = result.IndexOf("]") - 1 - result.IndexOf("["); - string session = result.Substring(result.IndexOf("[") + 1, arr); + int arr = result.IndexOf(']') - 1 - result.IndexOf('['); + string session = result.Substring(result.IndexOf('[') + 1, arr); return session; } @@ -681,43 +739,47 @@ public string InstallCreate(DeviceData device, string packageName = null, params public void InstallWrite(DeviceData device, Stream apk, string apkName, string session) { EnsureDevice(device); - ExceptionExtensions.ThrowIfNull(apk); + ExceptionExtensions.ThrowIfNull(apkName); + ExceptionExtensions.ThrowIfNull(session); if (!apk.CanRead || !apk.CanSeek) { throw new ArgumentOutOfRangeException(nameof(apk), "The apk stream must be a readable and seekable stream"); } - ExceptionExtensions.ThrowIfNull(session); - - ExceptionExtensions.ThrowIfNull(apkName); - - StringBuilder requestBuilder = new(); - requestBuilder.Append($"exec:cmd package 'install-write'"); - - // add size parameter [required for streaming installs] - // do last to override any user specified value - requestBuilder.Append($" -S {apk.Length}"); - - requestBuilder.Append($" {session} {apkName}.apk"); + StringBuilder requestBuilder = + new StringBuilder().Append($"exec:cmd package 'install-write'") + // add size parameter [required for streaming installs] + // do last to override any user specified value + .AppendFormat(" -S {0}", apk.Length) + .AppendFormat(" {0} {1}.apk", session, apkName); using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); socket.SendAdbRequest(requestBuilder.ToString()); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); byte[] buffer = new byte[32 * 1024]; int read = 0; - while ((read = apk.Read(buffer, 0, buffer.Length)) > 0) + while ((read = apk.Read(buffer)) > 0) { +#if HAS_BUFFERS + socket.Send(buffer.AsSpan(0, read)); +#else socket.Send(buffer, read); +#endif } read = socket.Read(buffer); - string value = Encoding.UTF8.GetString(buffer, 0, read); + string value = +#if HAS_BUFFERS + Encoding.UTF8.GetString(buffer.AsSpan(0, read)); +#else + Encoding.UTF8.GetString(buffer, 0, read); +#endif if (!value.Contains("Success")) { @@ -734,11 +796,42 @@ public void InstallCommit(DeviceData device, string session) socket.SetDevice(device); socket.SendAdbRequest($"exec:cmd package 'install-commit' {session}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); using StreamReader reader = new(socket.GetShellStream(), Encoding); - string result = reader.ReadLine(); - if (!result.Contains("Success")) + string? result = reader.ReadLine(); + if (result?.Contains("Success") != true) + { + throw new AdbException(reader.ReadToEnd()); + } + } + + /// + public void Uninstall(DeviceData device, string packageName, params string[] arguments) + { + EnsureDevice(device); + + StringBuilder requestBuilder = new StringBuilder().Append("exec:cmd package 'uninstall'"); + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + _ = requestBuilder.AppendFormat(" {0}", packageName); + + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SetDevice(device); + + socket.SendAdbRequest(requestBuilder.ToString()); + _ = socket.ReadAdbResponse(); + + using StreamReader reader = new(socket.GetShellStream(), Encoding); + string? result = reader.ReadLine(); + if (result?.Contains("Success") != true) { throw new AdbException(reader.ReadToEnd()); } @@ -751,11 +844,10 @@ public IEnumerable GetFeatureSet(DeviceData device) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SendAdbRequest($"host-serial:{device.Serial}:features"); - - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); string features = socket.ReadString(); - IEnumerable featureList = features.Trim().Split(new char[] { '\n', ',' }); + IEnumerable featureList = features.Trim().Split('\n', ','); return featureList; } @@ -763,30 +855,32 @@ public IEnumerable GetFeatureSet(DeviceData device) public string DumpScreenString(DeviceData device) { EnsureDevice(device); + using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); + socket.SendAdbRequest("shell:uiautomator dump /dev/tty"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string xmlString = reader.ReadToEnd() .Replace("Events injected: 1\r\n", string.Empty) .Replace("UI hierchary dumped to: /dev/tty", string.Empty) .Trim(); + if (string.IsNullOrEmpty(xmlString) || xmlString.StartsWith(" - public XmlDocument DumpScreen(DeviceData device) + public XmlDocument? DumpScreen(DeviceData device) { + EnsureDevice(device); XmlDocument doc = new(); string xmlString = DumpScreenString(device); if (!string.IsNullOrEmpty(xmlString)) @@ -799,8 +893,9 @@ public XmlDocument DumpScreen(DeviceData device) #if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER /// - public Windows.Data.Xml.Dom.XmlDocument DumpScreenWinRT(DeviceData device) + public Windows.Data.Xml.Dom.XmlDocument? DumpScreenWinRT(DeviceData device) { + EnsureDevice(device); Windows.Data.Xml.Dom.XmlDocument doc = new(); string xmlString = DumpScreenString(device); if (!string.IsNullOrEmpty(xmlString)) @@ -813,21 +908,24 @@ public Windows.Data.Xml.Dom.XmlDocument DumpScreenWinRT(DeviceData device) #endif /// - public void Click(DeviceData device, Cords cords) + public void Click(DeviceData device, Point cords) { EnsureDevice(device); using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); + socket.SendAdbRequest($"shell:input tap {cords.X} {cords.Y}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string result = reader.ReadToEnd().TrimStart(); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } @@ -840,15 +938,18 @@ public void Click(DeviceData device, int x, int y) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); + socket.SendAdbRequest($"shell:input tap {x} {y}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string result = reader.ReadToEnd().TrimStart(); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } @@ -861,15 +962,18 @@ public void Swipe(DeviceData device, Element first, Element second, long speed) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); - socket.SendAdbRequest($"shell:input swipe {first.Cords.X} {first.Cords.Y} {second.Cords.X} {second.Cords.Y} {speed}"); - AdbResponse response = socket.ReadAdbResponse(); + + socket.SendAdbRequest($"shell:input swipe {first.Center.X} {first.Center.Y} {second.Center.X} {second.Center.Y} {speed}"); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string result = reader.ReadToEnd().TrimStart(); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } @@ -882,37 +986,54 @@ public void Swipe(DeviceData device, int x1, int y1, int x2, int y2, long speed) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); + socket.SendAdbRequest($"shell:input swipe {x1} {y1} {x2} {y2} {speed}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string result = reader.ReadToEnd().TrimStart(); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new ElementNotFoundException("Coordinates of element is invalid"); } } /// - public bool IsCurrentApp(DeviceData device, string packageName) + public bool IsAppRunning(DeviceData device, string packageName) { - ConsoleOutputReceiver receiver = new(); - ExecuteRemoteCommand($"dumpsys activity activities | grep mResumedActivity", device, receiver); - string response = receiver.ToString().Trim(); - return response.ToString().Contains(packageName); + EnsureDevice(device); + + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SetDevice(device); + + socket.SendAdbRequest($"shell:pidof {packageName}"); + _ = socket.ReadAdbResponse(); + + using StreamReader reader = new(socket.GetShellStream(), Encoding); + string? result = reader.ReadToEnd().TrimStart().Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + bool intParsed = int.TryParse(result, out int pid); + return intParsed && pid > 0; } /// - public bool IsAppRunning(DeviceData device, string packageName) + public bool IsAppInForeground(DeviceData device, string packageName) { - ConsoleOutputReceiver receiver = new(); - ExecuteRemoteCommand($"pidof {packageName}", device, receiver); - string response = receiver.ToString().Trim(); - bool intParsed = int.TryParse(response, out int pid); - return intParsed && pid > 0; + EnsureDevice(device); + + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SetDevice(device); + + socket.SendAdbRequest($"shell:dumpsys activity activities | grep mResumedActivity"); + _ = socket.ReadAdbResponse(); + + using StreamReader reader = new(socket.GetShellStream(), Encoding); + string result = reader.ReadToEnd(); + return result.Contains(packageName); } /// @@ -921,7 +1042,7 @@ public AppStatus GetAppStatus(DeviceData device, string packageName) EnsureDevice(device); // Check if the app is in foreground - bool currentApp = IsCurrentApp(device, packageName); + bool currentApp = IsAppInForeground(device, packageName); if (currentApp) { return AppStatus.Foreground; @@ -933,31 +1054,36 @@ public AppStatus GetAppStatus(DeviceData device, string packageName) } /// - public Element FindElement(DeviceData device, string xpath = "hierarchy/node", TimeSpan timeout = default) + public Element? FindElement(DeviceData device, string xpath = "hierarchy/node", TimeSpan timeout = default) { EnsureDevice(device); Stopwatch stopwatch = new(); stopwatch.Start(); - while (timeout == TimeSpan.Zero || stopwatch.Elapsed < timeout) + do { - XmlDocument doc = DumpScreen(device); - if (doc != null) + try { - XmlNode xmlNode = doc.SelectSingleNode(xpath); - if (xmlNode != null) + XmlDocument? doc = DumpScreen(device); + if (doc != null) { - Element element = Element.FromXmlNode(this, device, xmlNode); - if (element != null) + XmlNode? xmlNode = doc.SelectSingleNode(xpath); + if (xmlNode != null) { - return element; + Element? element = Element.FromXmlNode(this, device, xmlNode); + if (element != null) + { + return element; + } } } } - if (timeout == TimeSpan.Zero) + catch (XmlException) { - break; + // Ignore XmlException and try again } + if (timeout == default) { break; } } + while (stopwatch.Elapsed < timeout); return null; } @@ -967,35 +1093,39 @@ public IEnumerable FindElements(DeviceData device, string xpath = "hier EnsureDevice(device); Stopwatch stopwatch = new(); stopwatch.Start(); - while (timeout == TimeSpan.Zero || stopwatch.Elapsed < timeout) + do { - XmlDocument doc = DumpScreen(device); + XmlDocument? doc = null; + + try + { + doc = DumpScreen(device); + } + catch (XmlException) + { + // Ignore XmlException and try again + } + if (doc != null) { - XmlNodeList xmlNodes = doc.SelectNodes(xpath); + XmlNodeList? xmlNodes = doc.SelectNodes(xpath); if (xmlNodes != null) { - bool isBreak = false; for (int i = 0; i < xmlNodes.Count; i++) { - Element element = Element.FromXmlNode(this, device, xmlNodes[i]); + Element? element = Element.FromXmlNode(this, device, xmlNodes[i]); if (element != null) { - isBreak = true; yield return element; } } - if (isBreak) - { - break; - } + break; } } - if (timeout == TimeSpan.Zero) - { - break; - } + + if (timeout == default) { break; } } + while (stopwatch.Elapsed < timeout); } /// @@ -1005,15 +1135,18 @@ public void SendKeyEvent(DeviceData device, string key) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); + socket.SendAdbRequest($"shell:input keyevent {key}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string result = reader.ReadToEnd().TrimStart(); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new InvalidKeyEventException("KeyEvent is invalid"); } @@ -1026,45 +1159,50 @@ public void SendText(DeviceData device, string text) using IAdbSocket socket = adbSocketFactory(EndPoint); socket.SetDevice(device); + socket.SendAdbRequest($"shell:input text {text}"); - AdbResponse response = socket.ReadAdbResponse(); + _ = socket.ReadAdbResponse(); + using StreamReader reader = new(socket.GetShellStream(), Encoding); string result = reader.ReadToEnd().TrimStart(); + if (result.StartsWith("java.lang.")) { throw JavaException.Parse(result); } - else if (result.IndexOf("ERROR", StringComparison.OrdinalIgnoreCase) != -1) // error or ERROR + else if (result.Contains("ERROR", StringComparison.OrdinalIgnoreCase)) // error or ERROR { throw new InvalidTextException(); } } - /// - public void ClearInput(DeviceData device, int charCount) + public void StartApp(DeviceData device, string packageName) { - SendKeyEvent(device, "KEYCODE_MOVE_END"); - ExecuteRemoteCommand("input keyevent " + Utilities.Join(" ", Enumerable.Repeat("KEYCODE_DEL ", charCount)), device, null); - } + EnsureDevice(device); - /// - public void StartApp(DeviceData device, string packageName) => - ExecuteRemoteCommand($"monkey -p {packageName} 1", device, null); + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SetDevice(device); - /// - public void StopApp(DeviceData device, string packageName) => - ExecuteRemoteCommand($"am force-stop {packageName}", device, null); + socket.SendAdbRequest($"shell:monkey -p {packageName} 1"); + _ = socket.ReadAdbResponse(); + } /// - public void BackBtn(DeviceData device) => SendKeyEvent(device, "KEYCODE_BACK"); + public void StopApp(DeviceData device, string packageName) + { + EnsureDevice(device); - /// - public void HomeBtn(DeviceData device) => SendKeyEvent(device, "KEYCODE_HOME"); + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SetDevice(device); + + socket.SendAdbRequest($"shell:am force-stop {packageName}"); + _ = socket.ReadAdbResponse(); + } /// - /// Sets default encoding (default - UTF8) + /// Sets default encoding (default - UTF8). /// - /// + /// The to set. public static void SetEncoding(Encoding encoding) => Encoding = encoding; /// @@ -1073,10 +1211,9 @@ public void StopApp(DeviceData device, string packageName) => /// if does not have a valid serial number. /// /// A object to validate. - protected static void EnsureDevice(DeviceData device) + protected static void EnsureDevice([NotNull] DeviceData? device) { ExceptionExtensions.ThrowIfNull(device); - if (string.IsNullOrEmpty(device.Serial)) { throw new ArgumentOutOfRangeException(nameof(device), "You must specific a serial number for the device"); @@ -1085,16 +1222,17 @@ protected static void EnsureDevice(DeviceData device) #if NET7_0_OR_GREATER [GeneratedRegex("<\\?xml(.?)*")] - private static partial Regex GetXMLRegex(); + private static partial Regex GetXmlRegex(); #else - private static Regex GetXMLRegex() => new("<\\?xml(.?)*"); + private static Regex GetXmlRegex() => new("<\\?xml(.?)*"); #endif } /// /// See as the class. /// - [Obsolete("AdvancedAdbClient is too long to remember. Please use AdbClient instead.")] + [Obsolete($"{nameof(AdvancedAdbClient)} is too long to remember. Please use {nameof(AdbClient)} instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] public class AdvancedAdbClient : AdbClient { } diff --git a/AdvancedSharpAdbClient/AdbCommandLineClient.Async.cs b/AdvancedSharpAdbClient/AdbCommandLineClient.Async.cs index c73885f2..05906f5f 100644 --- a/AdvancedSharpAdbClient/AdbCommandLineClient.Async.cs +++ b/AdvancedSharpAdbClient/AdbCommandLineClient.Async.cs @@ -3,11 +3,11 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading; namespace AdvancedSharpAdbClient @@ -22,19 +22,15 @@ public partial class AdbCommandLineClient public virtual async Task GetVersionAsync(CancellationToken cancellationToken = default) { // Run the adb.exe version command and capture the output. - List standardOutput = new(); - - await RunAdbProcessAsync("version", null, standardOutput, cancellationToken); + List standardOutput = []; + await RunAdbProcessAsync("version", null, standardOutput, cancellationToken).ConfigureAwait(false); // Parse the output to get the version. Version version = GetVersionFromOutput(standardOutput) ?? throw new AdbException($"The version of the adb executable at {AdbPath} could not be determined."); - if (version < AdbServer.RequiredAdbVersion) { AdbException ex = new($"Required minimum version of adb: {AdbServer.RequiredAdbVersion}. Current version is {version}"); -#if HAS_LOGGER logger.LogError(ex, ex.Message); -#endif throw ex; } @@ -48,13 +44,8 @@ public virtual async Task GetVersionAsync(CancellationToken cancellatio /// A which represents the asynchronous operation. public virtual async Task StartServerAsync(CancellationToken cancellationToken = default) { - int status = await RunAdbProcessInnerAsync("start-server", null, null, cancellationToken); - - if (status == 0) - { - return; - } - + int status = await RunAdbProcessInnerAsync("start-server", null, null, cancellationToken).ConfigureAwait(false); + if (status == 0) { return; } #if HAS_PROCESS && !WINDOWS_UWP try { @@ -85,10 +76,9 @@ public virtual async Task StartServerAsync(CancellationToken cancellationToken = // This platform does not support getting a list of processes. } #endif - // Try again. This time, we don't call "Inner", and an exception will be thrown if the start operation fails // again. We'll let that exception bubble up the stack. - await RunAdbProcessAsync("start-server", null, null, cancellationToken); + await RunAdbProcessAsync("start-server", null, null, cancellationToken).ConfigureAwait(false); } /// @@ -105,14 +95,10 @@ public virtual async Task StartServerAsync(CancellationToken cancellationToken = /// Use this command only for adb commands that return immediately, such as /// adb version. This operation times out after 5 seconds. /// The process exited with an exit code other than 0. - protected virtual async Task RunAdbProcessAsync(string command, List errorOutput, List standardOutput, CancellationToken cancellationToken = default) + protected virtual async Task RunAdbProcessAsync(string command, ICollection? errorOutput, ICollection? standardOutput, CancellationToken cancellationToken = default) { - int status = await RunAdbProcessInnerAsync(command, errorOutput, standardOutput, cancellationToken); - - if (status != 0) - { - throw new AdbException($"The adb process returned error code {status} when running command {command}"); - } + int status = await RunAdbProcessInnerAsync(command, errorOutput, standardOutput, cancellationToken).ConfigureAwait(false); + if (status != 0) { throw new AdbException($"The adb process returned error code {status} when running command {command}"); } } /// @@ -128,13 +114,66 @@ protected virtual async Task RunAdbProcessAsync(string command, List err /// A which return the return code of the adb process. /// Use this command only for adb commands that return immediately, such as /// adb version. This operation times out after 5 seconds. - protected virtual async Task RunAdbProcessInnerAsync(string command, List errorOutput, List standardOutput, CancellationToken cancellationToken = default) + protected virtual async Task RunAdbProcessInnerAsync(string command, ICollection? errorOutput, ICollection? standardOutput, CancellationToken cancellationToken = default) { ExceptionExtensions.ThrowIfNull(command); + return await RunProcessAsync(AdbPath, command, errorOutput, standardOutput, cancellationToken).ConfigureAwait(false); + } + + /// + /// Runs process, invoking a specific command, and reads the standard output and standard error output. + /// + /// The return code of the process. +#if !HAS_PROCESS + [DoesNotReturn] +#endif + protected virtual async Task RunProcessAsync(string filename, string command, ICollection? errorOutput, ICollection? standardOutput, CancellationToken cancellationToken = default) + { +#if HAS_PROCESS + ProcessStartInfo psi = new(filename, command) + { + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + using Process process = Process.Start(psi) ?? throw new AdbException($"The adb process could not be started. The process returned null when starting {filename} {command}"); + + string standardErrorString = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + string standardOutputString = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - int status = await CrossPlatformFunc.RunProcessAsync(AdbPath, command, errorOutput, standardOutput, cancellationToken); + errorOutput?.AddRange(standardErrorString.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + standardOutput?.AddRange(standardOutputString.Split(separator, StringSplitOptions.RemoveEmptyEntries)); - return status; +#if NET5_0_OR_GREATER + using (CancellationTokenSource completionSource = new(TimeSpan.FromMilliseconds(5000))) + { + try + { + await process.WaitForExitAsync(completionSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (completionSource.IsCancellationRequested) + { + if (!process.HasExited) + { + process.Kill(); + } + } + } +#else + if (!process.WaitForExit(5000)) + { + process.Kill(); + } +#endif + // get the return code from the process + return process.ExitCode; +#else + await Task.CompletedTask; + throw new PlatformNotSupportedException("This platform is not support System.Diagnostics.Process. You can start adb server by running `adb start-server` manually."); +#endif } } } diff --git a/AdvancedSharpAdbClient/AdbCommandLineClient.cs b/AdvancedSharpAdbClient/AdbCommandLineClient.cs index 35e1720f..9c9c5b66 100644 --- a/AdvancedSharpAdbClient/AdbCommandLineClient.cs +++ b/AdvancedSharpAdbClient/AdbCommandLineClient.cs @@ -2,11 +2,11 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.RegularExpressions; @@ -22,73 +22,44 @@ public partial class AdbCommandLineClient : IAdbCommandLineClient /// protected const string AdbVersionPattern = "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"; -#if HAS_LOGGER + private static readonly char[] separator = Extensions.NewLineSeparator; + /// /// The logger to use when logging messages. /// protected readonly ILogger logger; -#endif -#if !HAS_LOGGER -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// /// Initializes a new instance of the class. /// /// The path to the adb.exe executable. /// Don't check adb file name when . /// The logger to use when logging. - public AdbCommandLineClient(string adbPath, bool isForce = false -#if HAS_LOGGER - , ILogger logger = null -#endif - ) + public AdbCommandLineClient(string adbPath, bool isForce = false, ILogger? logger = null) { - if (adbPath.IsNullOrWhiteSpace()) + if (StringExtensions.IsNullOrWhiteSpace(adbPath)) { throw new ArgumentNullException(nameof(adbPath)); } if (!isForce) { - bool isWindows = Utilities.IsWindowsPlatform(); - bool isUnix = Utilities.IsUnixPlatform(); - - if (isWindows) - { - if (!string.Equals(Path.GetFileName(adbPath), "adb.exe", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentOutOfRangeException(nameof(adbPath), $"{adbPath} does not seem to be a valid adb.exe executable. The path must end with `adb.exe`"); - } - } - else if (isUnix) - { - if (!string.Equals(Path.GetFileName(adbPath), "adb", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentOutOfRangeException(nameof(adbPath), $"{adbPath} does not seem to be a valid adb executable. The path must end with `adb`"); - } - } - else - { - throw new NotSupportedException("SharpAdbClient only supports launching adb.exe on Windows, Mac OS and Linux"); - } + EnsureIsValidAdbFile(adbPath); } - this.EnsureIsValidAdbFile(adbPath); + if (!CheckFileExists(adbPath)) + { + throw new FileNotFoundException($"The adb.exe executable could not be found at {adbPath}"); + } AdbPath = adbPath; -#if HAS_LOGGER - this.logger = logger ?? NullLogger.Instance; -#endif + this.logger = logger ?? LoggerProvider.CreateLogger(); } -#if !HAS_LOGGER -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// /// Gets the path to the adb.exe executable. /// - public string AdbPath { get; private set; } + public string AdbPath { get; protected set; } /// /// Queries adb for its version number and checks it against . @@ -97,19 +68,15 @@ public AdbCommandLineClient(string adbPath, bool isForce = false public virtual Version GetVersion() { // Run the adb.exe version command and capture the output. - List standardOutput = new(); - + List standardOutput = []; RunAdbProcess("version", null, standardOutput); // Parse the output to get the version. Version version = GetVersionFromOutput(standardOutput) ?? throw new AdbException($"The version of the adb executable at {AdbPath} could not be determined."); - if (version < AdbServer.RequiredAdbVersion) { AdbException ex = new($"Required minimum version of adb: {AdbServer.RequiredAdbVersion}. Current version is {version}"); -#if HAS_LOGGER logger.LogError(ex, ex.Message); -#endif throw ex; } @@ -122,12 +89,7 @@ public virtual Version GetVersion() public virtual void StartServer() { int status = RunAdbProcessInner("start-server", null, null); - - if (status == 0) - { - return; - } - + if (status == 0) { return; } #if HAS_PROCESS && !WINDOWS_UWP try { @@ -158,21 +120,49 @@ public virtual void StartServer() // This platform does not support getting a list of processes. } #endif - // Try again. This time, we don't call "Inner", and an exception will be thrown if the start operation fails // again. We'll let that exception bubble up the stack. RunAdbProcess("start-server", null, null); } /// - public virtual bool IsValidAdbFile(string adbPath) => CrossPlatformFunc.CheckFileExists(adbPath); + public virtual bool CheckFileExists(string adbPath) => Factories.CheckFileExists(adbPath); + + /// + /// Throws an error if the path does not point to a valid instance of adb.exe. + /// + /// The path to validate. + protected virtual void EnsureIsValidAdbFile(string adbPath) + { + bool isWindows = Extensions.IsWindowsPlatform(); + bool isUnix = Extensions.IsUnixPlatform(); + + if (isWindows) + { + if (!string.Equals(Path.GetFileName(adbPath), "adb.exe", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentOutOfRangeException(nameof(adbPath), $"{adbPath} does not seem to be a valid adb.exe executable. The path must end with `adb.exe`"); + } + } + else if (isUnix) + { + if (!string.Equals(Path.GetFileName(adbPath), "adb", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentOutOfRangeException(nameof(adbPath), $"{adbPath} does not seem to be a valid adb executable. The path must end with `adb`"); + } + } + else + { + throw new NotSupportedException("SharpAdbClient only supports launching adb.exe on Windows, Mac OS and Linux"); + } + } /// /// Parses the output of the adb.exe version command and determines the adb version. /// /// The output of the adb.exe version command. /// A object that represents the version of the adb command line client. - internal static Version GetVersionFromOutput(IEnumerable output) + protected static Version? GetVersionFromOutput(IEnumerable output) { Regex regex = AdbVersionRegex(); foreach (string line in output) @@ -193,7 +183,6 @@ internal static Version GetVersionFromOutput(IEnumerable output) return new Version(majorVersion, minorVersion, microVersion); } } - return null; } @@ -209,14 +198,10 @@ internal static Version GetVersionFromOutput(IEnumerable output) /// Use this command only for adb commands that return immediately, such as /// adb version. This operation times out after 5 seconds. /// The process exited with an exit code other than 0. - protected virtual void RunAdbProcess(string command, List errorOutput, List standardOutput) + protected virtual void RunAdbProcess(string command, ICollection? errorOutput, ICollection? standardOutput) { int status = RunAdbProcessInner(command, errorOutput, standardOutput); - - if (status != 0) - { - throw new AdbException($"The adb process returned error code {status} when running command {command}"); - } + if (status != 0) { throw new AdbException($"The adb process returned error code {status} when running command {command}"); } } /// @@ -231,13 +216,49 @@ protected virtual void RunAdbProcess(string command, List errorOutput, L /// The return code of the adb process. /// Use this command only for adb commands that return immediately, such as /// adb version. This operation times out after 5 seconds. - protected virtual int RunAdbProcessInner(string command, List errorOutput, List standardOutput) + protected virtual int RunAdbProcessInner(string command, ICollection? errorOutput, ICollection? standardOutput) { ExceptionExtensions.ThrowIfNull(command); + return RunProcess(AdbPath, command, errorOutput, standardOutput); + } + + /// + /// Runs process, invoking a specific command, and reads the standard output and standard error output. + /// + /// The return code of the process. +#if !HAS_PROCESS + [DoesNotReturn] +#endif + protected virtual int RunProcess(string filename, string command, ICollection? errorOutput, ICollection? standardOutput) + { +#if HAS_PROCESS + ProcessStartInfo psi = new(filename, command) + { + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true + }; - int status = CrossPlatformFunc.RunProcess(AdbPath, command, errorOutput, standardOutput); + using Process process = Process.Start(psi) ?? throw new AdbException($"The adb process could not be started. The process returned null when starting {filename} {command}"); - return status; + string standardErrorString = process.StandardError.ReadToEnd(); + string standardOutputString = process.StandardOutput.ReadToEnd(); + + errorOutput?.AddRange(standardErrorString.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + standardOutput?.AddRange(standardOutputString.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + + // get the return code from the process + if (!process.WaitForExit(5000)) + { + process.Kill(); + } + + return process.ExitCode; +#else + throw new PlatformNotSupportedException("This platform is not support System.Diagnostics.Process. You can start adb server by running `adb start-server` manually."); +#endif } #if NET7_0_OR_GREATER diff --git a/AdvancedSharpAdbClient/AdbServer.Async.cs b/AdvancedSharpAdbClient/AdbServer.Async.cs index 3dc11978..bfbf35bd 100644 --- a/AdvancedSharpAdbClient/AdbServer.Async.cs +++ b/AdvancedSharpAdbClient/AdbServer.Async.cs @@ -3,8 +3,8 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Globalization; using System.Net.Sockets; using System.Threading; @@ -13,89 +13,79 @@ namespace AdvancedSharpAdbClient public partial class AdbServer { /// - public virtual async Task StartServerAsync(string adbPath, bool restartServerIfNewer, CancellationToken cancellationToken = default) + public virtual async Task StartServerAsync(string adbPath, bool restartServerIfNewer = false, CancellationToken cancellationToken = default) { - AdbServerStatus serverStatus = await GetStatusAsync(cancellationToken); - Version commandLineVersion = null; - - IAdbCommandLineClient commandLineClient = adbCommandLineClientFactory(adbPath); - - if (commandLineClient.IsValidAdbFile(adbPath)) + if (IsStarting) { return StartServerResult.Starting; } + try { - cachedAdbPath = adbPath; - commandLineVersion = await commandLineClient.GetVersionAsync(cancellationToken); - } + AdbServerStatus serverStatus = await GetStatusAsync(cancellationToken).ConfigureAwait(false); + Version? commandLineVersion = null; - // If the server is running, and no adb path is provided, check if we have the minimum version - if (adbPath == null) - { - return !serverStatus.IsRunning - ? throw new AdbException("The adb server is not running, but no valid path to the adb.exe executable was provided. The adb server cannot be started.") - : serverStatus.Version >= RequiredAdbVersion - ? StartServerResult.AlreadyRunning - : throw new AdbException($"The adb daemon is running an outdated version ${commandLineVersion}, but not valid path to the adb.exe executable was provided. A more recent version of the adb server cannot be started."); - } + IAdbCommandLineClient commandLineClient = adbCommandLineClientFactory(adbPath); + CheckFileExists = commandLineClient.CheckFileExists; - if (serverStatus.IsRunning - && ((serverStatus.Version < RequiredAdbVersion) - || ((serverStatus.Version < commandLineVersion) && restartServerIfNewer))) - { - ExceptionExtensions.ThrowIfNull(adbPath); + if (commandLineClient.CheckFileExists(adbPath)) + { + CachedAdbPath = adbPath; + commandLineVersion = await commandLineClient.GetVersionAsync(cancellationToken).ConfigureAwait(false); + } - await adbClient.KillAdbAsync(cancellationToken); - serverStatus.IsRunning = false; - serverStatus.Version = null; + // If the server is running, and no adb path is provided, check if we have the minimum version + if (adbPath == null) + { + return !serverStatus.IsRunning + ? throw new AdbException("The adb server is not running, but no valid path to the adb.exe executable was provided. The adb server cannot be started.") + : serverStatus.Version >= RequiredAdbVersion + ? StartServerResult.AlreadyRunning + : throw new AdbException($"The adb daemon is running an outdated version ${commandLineVersion}, but not valid path to the adb.exe executable was provided. A more recent version of the adb server cannot be started."); + } - await commandLineClient.StartServerAsync(cancellationToken); - return StartServerResult.RestartedOutdatedDaemon; - } - else if (!serverStatus.IsRunning) - { - ExceptionExtensions.ThrowIfNull(adbPath); + if (serverStatus.IsRunning) + { + if (serverStatus.Version < RequiredAdbVersion + || (restartServerIfNewer && serverStatus.Version < commandLineVersion)) + { + ExceptionExtensions.ThrowIfNull(adbPath); + + await StopServerAsync(cancellationToken); + await commandLineClient.StartServerAsync(cancellationToken); + return StartServerResult.RestartedOutdatedDaemon; + } + else + { + return StartServerResult.AlreadyRunning; + } + } + else + { + ExceptionExtensions.ThrowIfNull(adbPath); - await commandLineClient.StartServerAsync(cancellationToken); - return StartServerResult.Started; + await commandLineClient.StartServerAsync(cancellationToken); + return StartServerResult.Started; + } } - else + finally { - return StartServerResult.AlreadyRunning; + IsStarting = false; } } /// - public Task RestartServerAsync(CancellationToken cancellationToken = default) => RestartServerAsync(null, cancellationToken); + public Task RestartServerAsync(CancellationToken cancellationToken = default) => + StartServerAsync(CachedAdbPath!, true, cancellationToken); /// - public virtual async Task RestartServerAsync(string adbPath, CancellationToken cancellationToken = default) - { - adbPath ??= cachedAdbPath; + public virtual Task RestartServerAsync(string adbPath, CancellationToken cancellationToken = default) => + StringExtensions.IsNullOrWhiteSpace(adbPath) ? RestartServerAsync(cancellationToken) : StartServerAsync(adbPath, true, cancellationToken); - if (!IsValidAdbFile(adbPath)) - { - throw new InvalidOperationException($"The adb server was not started via {nameof(AdbServer)}.{nameof(this.StartServer)} or no path to adb was specified. The adb server cannot be restarted."); - } - - ManualResetEvent manualResetEvent = null; - await Utilities.Run(() => - { - lock (RestartLock) - { - manualResetEvent = new(false); - } - }, cancellationToken); - - _ = Utilities.Run(() => - { - lock (RestartLock) - { - manualResetEvent.WaitOne(); - } - }, cancellationToken); + /// + public virtual async Task StopServerAsync(CancellationToken cancellationToken = default) + { + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SendAdbRequestAsync("host:kill", cancellationToken).ConfigureAwait(false); - StartServerResult result = await StartServerAsync(adbPath, false, cancellationToken); - manualResetEvent.Set(); - manualResetEvent.Dispose(); - return result; + // The host will immediately close the connection after the kill + // command has been sent; no need to read the response. } /// @@ -104,7 +94,12 @@ public virtual async Task GetStatusAsync(CancellationToken canc // Try to connect to a running instance of the adb server try { - int versionCode = await adbClient.GetAdbVersionAsync(cancellationToken); + using IAdbSocket socket = adbSocketFactory(EndPoint); + await socket.SendAdbRequestAsync("host:version", cancellationToken).ConfigureAwait(false); + AdbResponse response = await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); + string version = await socket.ReadStringAsync(cancellationToken).ConfigureAwait(false); + + int versionCode = int.Parse(version, NumberStyles.HexNumber); return new AdbServerStatus(true, new Version(1, 0, versionCode)); } catch (AggregateException ex) diff --git a/AdvancedSharpAdbClient/AdbServer.cs b/AdvancedSharpAdbClient/AdbServer.cs index a08a9d72..1ac66757 100644 --- a/AdvancedSharpAdbClient/AdbServer.cs +++ b/AdvancedSharpAdbClient/AdbServer.cs @@ -2,8 +2,9 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Globalization; +using System.Net; using System.Net.Sockets; namespace AdvancedSharpAdbClient @@ -29,8 +30,8 @@ public partial class AdbServer : IAdbServer /// /// No connection could be made because the target computer actively refused it.This usually /// results from trying to connect to a service that is inactive on the foreign host—that is, - /// one with no server application running. - internal const int ConnectionRefused = 10061; + /// one with no server application running. + public const int ConnectionRefused = 10061; /// /// The error code that is returned by the when the connection was reset by the peer. @@ -39,139 +40,208 @@ public partial class AdbServer : IAdbServer /// remote host is suddenly stopped, the host is rebooted, the host or remote network interface is disabled, or the remote /// host uses a hard close. This error may also result if a connection was broken due to keep-alive activity detecting /// a failure while one or more operations are in progress. - internal const int ConnectionReset = 10054; + public const int ConnectionReset = 10054; /// - /// Throws an error if the path does not point to a valid instance of adb.exe. + /// The to create . /// - internal static Func IsValidAdbFile = CrossPlatformFunc.CheckFileExists; + protected readonly Func adbSocketFactory; /// - /// A lock used to ensure only one caller at a time can attempt to restart adb. + /// Gets or sets a function that returns a new instance of a class that implements the + /// interface, that can be used to interact with the + /// adb.exe command line client. /// - protected static readonly object RestartLock = new(); + protected readonly Func adbCommandLineClientFactory; /// - /// The path to the adb server. Cached from calls to . Used when restarting - /// the server to figure out where adb is located. + /// Initializes a new instance of the class. /// - protected static string cachedAdbPath; + public AdbServer() + : this(new IPEndPoint(IPAddress.Loopback, AdbClient.AdbServerPort), Factories.AdbSocketFactory, Factories.AdbCommandLineClientFactory) + { + } /// - /// The current ADB client that manages the connection. + /// Initializes a new instance of the class. /// - protected readonly IAdbClient adbClient; + /// A connection to an adb server. + public AdbServer(IAdbClient adbClient) + : this(adbClient.EndPoint, Factories.AdbSocketFactory, Factories.AdbCommandLineClientFactory) + { + } /// - /// Gets or sets a function that returns a new instance of a class that implements the - /// interface, that can be used to interact with the - /// adb.exe command line client. + /// Initializes a new instance of the class. /// - protected readonly Func adbCommandLineClientFactory; + /// The at which the adb server is listening. + public AdbServer(EndPoint endPoint) + : this(endPoint, Factories.AdbSocketFactory, Factories.AdbCommandLineClientFactory) + { + } /// /// Initializes a new instance of the class. /// - public AdbServer() : this(new AdbClient(), Factories.AdbCommandLineClientFactory) + /// The host address at which the adb server is listening. + /// The port at which the adb server is listening. + public AdbServer(string host, int port) + : this(Extensions.CreateDnsEndPoint(host, port), Factories.AdbSocketFactory, Factories.AdbCommandLineClientFactory) { } /// /// Initializes a new instance of the class. /// - /// The current ADB client that manages the connection. - public AdbServer(IAdbClient adbClient) : this(adbClient, Factories.AdbCommandLineClientFactory) + /// A connection to an adb server. + /// The to create . + /// The to create . + public AdbServer(IAdbClient adbClient, Func adbSocketFactory, Func adbCommandLineClientFactory) + : this(adbClient.EndPoint, adbSocketFactory, adbCommandLineClientFactory) { } /// /// Initializes a new instance of the class. /// + /// The at which the adb server is listening. + /// The to create . /// The to create . - public AdbServer(Func adbCommandLineClientFactory) : this(new AdbClient(), adbCommandLineClientFactory) + public AdbServer(EndPoint endPoint, Func adbSocketFactory, Func adbCommandLineClientFactory) { + ExceptionExtensions.ThrowIfNull(endPoint); + + if (endPoint is not (IPEndPoint or DnsEndPoint)) + { + throw new NotSupportedException("Only TCP endpoints are supported"); + } + + EndPoint = endPoint; + this.adbSocketFactory = adbSocketFactory ?? throw new ArgumentNullException(nameof(adbSocketFactory)); + this.adbCommandLineClientFactory = adbCommandLineClientFactory ?? throw new ArgumentNullException(nameof(adbCommandLineClientFactory)); } /// /// Initializes a new instance of the class. /// - /// The current ADB client that manages the connection. + /// The host address at which the adb server is listening. + /// The port at which the adb server is listening. + /// The to create . /// The to create . - public AdbServer(IAdbClient adbClient, Func adbCommandLineClientFactory) + public AdbServer(string host, int port, Func adbSocketFactory, Func adbCommandLineClientFactory) + : this(Extensions.CreateDnsEndPoint(host, port), adbSocketFactory, adbCommandLineClientFactory) { - this.adbCommandLineClientFactory = adbCommandLineClientFactory ?? throw new ArgumentNullException(nameof(adbCommandLineClientFactory)); - this.adbClient = adbClient ?? throw new ArgumentNullException(nameof(adbClient)); } + /// + /// Initializes a new instance of the class. + /// + /// The to create . + /// The to create . + public AdbServer(Func adbSocketFactory, Func adbCommandLineClientFactory) + : this(new IPEndPoint(IPAddress.Loopback, AdbClient.AdbServerPort), adbSocketFactory, adbCommandLineClientFactory) + { + } + + /// + /// The path to the adb server. Cached from calls to . Used when restarting + /// the server to figure out where adb is located. + /// + protected static string? CachedAdbPath { get; set; } + /// /// Gets or sets the default instance of the interface. /// public static IAdbServer Instance { get; set; } = new AdbServer(); - /// - public virtual StartServerResult StartServer(string adbPath, bool restartServerIfNewer) - { - AdbServerStatus serverStatus = GetStatus(); - Version commandLineVersion = null; + /// + /// Throws an error if the path does not point to a valid instance of adb.exe. + /// + protected static Func CheckFileExists { get; set; } = Factories.CheckFileExists; - IAdbCommandLineClient commandLineClient = adbCommandLineClientFactory(adbPath); + /// + /// if is starting adb server; otherwise, . + /// + protected static bool IsStarting { get; set; } = false; - if (commandLineClient.IsValidAdbFile(adbPath)) - { - cachedAdbPath = adbPath; - commandLineVersion = commandLineClient.GetVersion(); - } + /// + /// Gets the at which the adb server is listening. + /// + public EndPoint EndPoint { get; protected set; } - // If the server is running, and no adb path is provided, check if we have the minimum version - if (adbPath == null) + /// + public virtual StartServerResult StartServer(string adbPath, bool restartServerIfNewer = false) + { + if (IsStarting) { return StartServerResult.Starting; } + try { - return !serverStatus.IsRunning - ? throw new AdbException("The adb server is not running, but no valid path to the adb.exe executable was provided. The adb server cannot be started.") - : serverStatus.Version >= RequiredAdbVersion - ? StartServerResult.AlreadyRunning - : throw new AdbException($"The adb daemon is running an outdated version ${commandLineVersion}, but not valid path to the adb.exe executable was provided. A more recent version of the adb server cannot be started."); - } + AdbServerStatus serverStatus = GetStatus(); + Version? commandLineVersion = null; - if (serverStatus.IsRunning - && ((serverStatus.Version < RequiredAdbVersion) - || ((serverStatus.Version < commandLineVersion) && restartServerIfNewer))) - { - ExceptionExtensions.ThrowIfNull(adbPath); + IAdbCommandLineClient commandLineClient = adbCommandLineClientFactory(adbPath); + CheckFileExists = commandLineClient.CheckFileExists; - adbClient.KillAdb(); - serverStatus.IsRunning = false; - serverStatus.Version = null; + if (commandLineClient.CheckFileExists(adbPath)) + { + CachedAdbPath = adbPath; + commandLineVersion = commandLineClient.GetVersion(); + } - commandLineClient.StartServer(); - return StartServerResult.RestartedOutdatedDaemon; - } - else if (!serverStatus.IsRunning) - { - ExceptionExtensions.ThrowIfNull(adbPath); + // If the server is running, and no adb path is provided, check if we have the minimum version + if (adbPath == null) + { + return !serverStatus.IsRunning + ? throw new AdbException("The adb server is not running, but no valid path to the adb.exe executable was provided. The adb server cannot be started.") + : serverStatus.Version >= RequiredAdbVersion + ? StartServerResult.AlreadyRunning + : throw new AdbException($"The adb daemon is running an outdated version ${commandLineVersion}, but not valid path to the adb.exe executable was provided. A more recent version of the adb server cannot be started."); + } + + if (serverStatus.IsRunning) + { + if (serverStatus.Version < RequiredAdbVersion + || (restartServerIfNewer && serverStatus.Version < commandLineVersion)) + { + ExceptionExtensions.ThrowIfNull(adbPath); + + StopServer(); + commandLineClient.StartServer(); + return StartServerResult.RestartedOutdatedDaemon; + } + else + { + return StartServerResult.AlreadyRunning; + } + } + else + { + ExceptionExtensions.ThrowIfNull(adbPath); - commandLineClient.StartServer(); - return StartServerResult.Started; + commandLineClient.StartServer(); + return StartServerResult.Started; + } } - else + finally { - return StartServerResult.AlreadyRunning; + IsStarting = false; } } /// - public virtual StartServerResult RestartServer(string adbPath = null) - { - adbPath ??= cachedAdbPath; + public virtual StartServerResult RestartServer() => StartServer(CachedAdbPath!, true); - if (!IsValidAdbFile(adbPath)) - { - throw new InvalidOperationException($"The adb server was not started via {nameof(AdbServer)}.{nameof(this.StartServer)} or no path to adb was specified. The adb server cannot be restarted."); - } + /// + public virtual StartServerResult RestartServer(string adbPath) => + StringExtensions.IsNullOrWhiteSpace(adbPath) ? RestartServer() : StartServer(adbPath, true); - lock (RestartLock) - { - return StartServer(adbPath, false); - } + /// + public virtual void StopServer() + { + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SendAdbRequest("host:kill"); + + // The host will immediately close the connection after the kill + // command has been sent; no need to read the response. } /// @@ -180,7 +250,12 @@ public virtual AdbServerStatus GetStatus() // Try to connect to a running instance of the adb server try { - int versionCode = adbClient.GetAdbVersion(); + using IAdbSocket socket = adbSocketFactory(EndPoint); + socket.SendAdbRequest("host:version"); + AdbResponse response = socket.ReadAdbResponse(); + string version = socket.ReadString(); + + int versionCode = int.Parse(version, NumberStyles.HexNumber); return new AdbServerStatus(true, new Version(1, 0, versionCode)); } catch (SocketException ex) diff --git a/AdvancedSharpAdbClient/AdbSocket.Async.cs b/AdvancedSharpAdbClient/AdbSocket.Async.cs index 34559e0b..391859a3 100644 --- a/AdvancedSharpAdbClient/AdbSocket.Async.cs +++ b/AdvancedSharpAdbClient/AdbSocket.Async.cs @@ -3,7 +3,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Globalization; using System.IO; @@ -15,27 +14,57 @@ namespace AdvancedSharpAdbClient public partial class AdbSocket { /// - public Task SendAsync(byte[] data, int length, CancellationToken cancellationToken = default) => SendAsync(data, 0, length, cancellationToken); + public +#if NET6_0_OR_GREATER + ValueTask +#else + Task +#endif + ReconnectAsync(CancellationToken cancellationToken = default) => ReconnectAsync(false, cancellationToken); /// - public virtual async Task SendAsync(byte[] data, int offset, int length, CancellationToken cancellationToken = default) + public virtual +#if NET6_0_OR_GREATER + ValueTask +#else + Task +#endif + ReconnectAsync(bool isForce, CancellationToken cancellationToken = default) => Socket.ReconnectAsync(isForce, cancellationToken); + + /// + public virtual async Task SendAsync(byte[] data, int length, CancellationToken cancellationToken = default) { try { - int count = await socket.SendAsync(data, offset, length != -1 ? length : data.Length, SocketFlags.None, cancellationToken); + int count = await Socket.SendAsync(data, length != -1 ? length : data.Length, SocketFlags.None, cancellationToken).ConfigureAwait(false); + if (count < 0) { throw new AdbException("channel EOF"); } } -#if HAS_LOGGER catch (SocketException ex) { logger.LogError(ex, ex.Message); -#else - catch (SocketException) + throw; + } + } + + /// + public virtual async Task SendAsync(byte[] data, int offset, int length, CancellationToken cancellationToken = default) + { + try { -#endif + int count = await Socket.SendAsync(data, offset, length != -1 ? length : data.Length - offset, SocketFlags.None, cancellationToken).ConfigureAwait(false); + + if (count < 0) + { + throw new AdbException("channel EOF"); + } + } + catch (SocketException ex) + { + logger.LogError(ex, ex.Message); throw; } } @@ -50,8 +79,8 @@ public virtual async Task SendSyncRequestAsync(SyncCommand command, string path, ExceptionExtensions.ThrowIfNull(path); byte[] pathBytes = AdbClient.Encoding.GetBytes(path); - await SendSyncRequestAsync(command, pathBytes.Length, cancellationToken); - _ = await WriteAsync(pathBytes, cancellationToken); + await SendSyncRequestAsync(command, pathBytes.Length, cancellationToken).ConfigureAwait(false); + _ = await WriteAsync(pathBytes, cancellationToken).ConfigureAwait(false); } /// @@ -71,8 +100,8 @@ public virtual async Task SendSyncRequestAsync(SyncCommand command, int length, Array.Reverse(lengthBytes); } - _ = await WriteAsync(commandBytes, cancellationToken); - _ = await WriteAsync(lengthBytes, cancellationToken); + _ = await WriteAsync(commandBytes, cancellationToken).ConfigureAwait(false); + _ = await WriteAsync(lengthBytes, cancellationToken).ConfigureAwait(false); } /// @@ -80,27 +109,27 @@ public virtual async Task SendAdbRequestAsync(string request, CancellationToken { byte[] data = AdbClient.FormAdbRequest(request); - if (!await WriteAsync(data, cancellationToken)) + if (!await WriteAsync(data, cancellationToken).ConfigureAwait(false)) { throw new IOException($"Failed sending the request '{request}' to ADB"); } } /// - public Task ReadAsync(byte[] data, CancellationToken cancellationToken = default) => - ReadAsync(data, data.Length, cancellationToken); + public virtual Task ReadAsync(byte[] data, int length, CancellationToken cancellationToken = default) => + ReadAsync(data, 0, length, cancellationToken); /// - public virtual async Task ReadAsync(byte[] data, int length, CancellationToken cancellationToken = default) + public virtual async Task ReadAsync(byte[] data, int offset, int length, CancellationToken cancellationToken = default) { - ExceptionExtensions.ThrowIfNegative(length); - ExceptionExtensions.ThrowIfNull(data); + ExceptionExtensions.ThrowIfNegative(offset); + length = length != -1 ? length : data.Length; ExceptionExtensions.ThrowIfLessThan(data.Length, length, nameof(data)); int count = -1; - int totalRead = 0; + int totalRead = offset; while (count != 0 && totalRead < length) { @@ -111,20 +140,16 @@ public virtual async Task ReadAsync(byte[] data, int length, CancellationTo int left = length - totalRead; int bufferLength = left < ReceiveBufferSize ? left : ReceiveBufferSize; - count = await socket.ReceiveAsync(data, totalRead, bufferLength, SocketFlags.None, cancellationToken).ConfigureAwait(false); + count = await Socket.ReceiveAsync(data, totalRead, bufferLength, SocketFlags.None, cancellationToken).ConfigureAwait(false); if (count < 0) { -#if HAS_LOGGER logger.LogError("read: channel EOF"); -#endif throw new AdbException("EOF"); } else if (count == 0) { -#if HAS_LOGGER logger.LogInformation("DONE with Read"); -#endif } else { @@ -145,7 +170,13 @@ public virtual async Task ReadStringAsync(CancellationToken cancellation { // The first 4 bytes contain the length of the string byte[] reply = new byte[4]; - _ = await ReadAsync(reply, cancellationToken).ConfigureAwait(false); + int read = await ReadAsync(reply, cancellationToken).ConfigureAwait(false); + + if (read == 0) + { + // There is no data to read + return string.Empty; + } // Convert the bytes to a hex string string lenHex = AdbClient.Encoding.GetString(reply); @@ -164,18 +195,23 @@ public virtual async Task ReadSyncStringAsync(CancellationToken cancella { // The first 4 bytes contain the length of the string byte[] reply = new byte[4]; - _ = await ReadAsync(reply, cancellationToken); + _ = await ReadAsync(reply, cancellationToken).ConfigureAwait(false); if (!BitConverter.IsLittleEndian) { Array.Reverse(reply); } - int len = BitConverter.ToInt32(reply, 0); + int len = +#if HAS_BUFFERS + BitConverter.ToInt32(reply); +#else + BitConverter.ToInt32(reply, 0); +#endif // And get the string reply = new byte[len]; - _ = await ReadAsync(reply, cancellationToken); + _ = await ReadAsync(reply, cancellationToken).ConfigureAwait(false); string value = AdbClient.Encoding.GetString(reply); return value; @@ -185,19 +221,18 @@ public virtual async Task ReadSyncStringAsync(CancellationToken cancella public virtual async Task ReadSyncResponseAsync(CancellationToken cancellationToken = default) { byte[] data = new byte[4]; - _ = await ReadAsync(data, cancellationToken); - + _ = await ReadAsync(data, cancellationToken).ConfigureAwait(false); return SyncCommandConverter.GetCommand(data); } /// public virtual async Task ReadAdbResponseAsync(CancellationToken cancellationToken = default) { - AdbResponse response = await ReadAdbResponseInnerAsync(cancellationToken); + AdbResponse response = await ReadAdbResponseInnerAsync(cancellationToken).ConfigureAwait(false); if (!response.IOSuccess || !response.Okay) { - socket.Dispose(); + Socket.Dispose(); throw new AdbException($"An error occurred while reading a response from ADB: {response.Message}", response); } @@ -211,11 +246,11 @@ public virtual async Task SetDeviceAsync(DeviceData device, CancellationToken ca // to a specific device if (device != null) { - await SendAdbRequestAsync($"host:transport:{device.Serial}", cancellationToken); + await SendAdbRequestAsync($"host:transport:{device.Serial}", cancellationToken).ConfigureAwait(false); try { - AdbResponse response = await ReadAdbResponseAsync(cancellationToken); + AdbResponse response = await ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } catch (AdbException e) { @@ -231,6 +266,92 @@ public virtual async Task SetDeviceAsync(DeviceData device, CancellationToken ca } } +#if HAS_BUFFERS + /// + public virtual async ValueTask SendAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) + { + try + { + int count = await Socket.SendAsync(data, SocketFlags.None, cancellationToken).ConfigureAwait(false); + if (count < 0) + { + throw new AdbException("channel EOF"); + } + } + catch (SocketException ex) + { + logger.LogError(ex, ex.Message); + throw; + } + } + + /// + public virtual async ValueTask ReadAsync(Memory data, CancellationToken cancellationToken = default) + { + ExceptionExtensions.ThrowIfNull(data); + + int count = -1; + int totalRead = 0; + int length = data.Length; + + while (count != 0 && totalRead < length) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + int left = length - totalRead; + int bufferLength = left < ReceiveBufferSize ? left : ReceiveBufferSize; + + count = await Socket.ReceiveAsync(data.Slice(totalRead, bufferLength), SocketFlags.None, cancellationToken).ConfigureAwait(false); + + if (count < 0) + { + logger.LogError("read: channel EOF"); + throw new AdbException("EOF"); + } + else if (count == 0) + { + logger.LogInformation("DONE with Read"); + } + else + { + totalRead += count; + } + } + catch (SocketException ex) + { + throw new AdbException($"An error occurred while receiving data from the adb server: {ex.Message}.", ex); + } + } + + return totalRead; + } +#else + /// + public virtual async Task SendAsync(byte[] data, CancellationToken cancellationToken = default) + { + try + { + int count = await Socket.SendAsync(data, SocketFlags.None, cancellationToken).ConfigureAwait(false); + + if (count < 0) + { + throw new AdbException("channel EOF"); + } + } + catch (SocketException ex) + { + logger.LogError(ex, ex.Message); + throw; + } + } + + /// + public virtual Task ReadAsync(byte[] data, CancellationToken cancellationToken = default) => + ReadAsync(data, 0, data.Length, cancellationToken); +#endif + /// /// Write until all data in "data" is written or the connection fails or times out. /// @@ -238,20 +359,19 @@ public virtual async Task SetDeviceAsync(DeviceData device, CancellationToken ca /// A which can be used to cancel the asynchronous task. /// Returns if all data was written; otherwise, . /// This uses the default time out value. +#if HAS_BUFFERS + protected virtual async ValueTask WriteAsync(Memory data, CancellationToken cancellationToken = default) +#else protected virtual async Task WriteAsync(byte[] data, CancellationToken cancellationToken = default) +#endif { try { - await SendAsync(data, -1, cancellationToken); + await SendAsync(data, cancellationToken).ConfigureAwait(false); } -#if HAS_LOGGER catch (IOException e) { logger.LogError(e, e.Message); -#else - catch (IOException) - { -#endif return false; } @@ -265,25 +385,19 @@ protected virtual async Task WriteAsync(byte[] data, CancellationToken can /// A that represents the response received from ADB. protected virtual async Task ReadAdbResponseInnerAsync(CancellationToken cancellationToken = default) { - AdbResponse rasps = new(); - byte[] reply = new byte[4]; - _ = await ReadAsync(reply, cancellationToken); - - rasps.IOSuccess = true; - - rasps.Okay = IsOkay(reply); + _ = await ReadAsync(reply, cancellationToken).ConfigureAwait(false); - if (!rasps.Okay) + if (IsOkay(reply)) { - string message = await ReadStringAsync(cancellationToken); - rasps.Message = message; -#if HAS_LOGGER - logger.LogError($"Got reply '{ReplyToString(reply)}', diag='{rasps.Message}'"); -#endif + return AdbResponse.OK; + } + else + { + string message = await ReadStringAsync(cancellationToken).ConfigureAwait(false); + logger.LogError("Got reply '{0}', diag='{1}'", ReplyToString(reply), message); + return AdbResponse.FromError(message); } - - return rasps; } } } diff --git a/AdvancedSharpAdbClient/AdbSocket.cs b/AdvancedSharpAdbClient/AdbSocket.cs index e8430f9e..15799621 100644 --- a/AdvancedSharpAdbClient/AdbSocket.cs +++ b/AdvancedSharpAdbClient/AdbSocket.cs @@ -2,8 +2,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; using System; using System.Globalization; using System.IO; @@ -13,7 +11,6 @@ namespace AdvancedSharpAdbClient { - /// /// Implements a client for the Android Debug Bridge client-server protocol. Using the client, you /// can send messages to and receive messages from the Android Debug Bridge. @@ -26,38 +23,22 @@ namespace AdvancedSharpAdbClient /// public partial class AdbSocket : IAdbSocket { - /// - /// The underlying TCP socket that manages the connection with the ADB server. - /// - protected readonly ITcpSocket socket; - -#if HAS_LOGGER /// /// The logger to use when logging messages. /// protected readonly ILogger logger; -#endif -#if !HAS_LOGGER -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// /// Initializes a new instance of the class. /// /// The at which the Android Debug Bridge is listening for clients. /// The logger to use when logging. - public AdbSocket(EndPoint endPoint -#if HAS_LOGGER - , ILogger logger = null -#endif - ) + public AdbSocket(EndPoint endPoint, ILogger? logger = null) { - socket = new TcpSocket(); - socket.Connect(endPoint); - socket.ReceiveBufferSize = ReceiveBufferSize; -#if HAS_LOGGER - this.logger = logger ?? NullLogger.Instance; -#endif + Socket = new TcpSocket(); + Socket.Connect(endPoint); + Socket.ReceiveBufferSize = ReceiveBufferSize; + this.logger = logger ?? LoggerProvider.CreateLogger(); } /// @@ -66,44 +47,20 @@ public AdbSocket(EndPoint endPoint /// The host address at which the Android Debug Bridge is listening for clients. /// The port at which the Android Debug Bridge is listening for clients. /// The logger to use when logging. - public AdbSocket(string host, int port -#if HAS_LOGGER - , ILogger logger = null -#endif - ) + public AdbSocket(string host, int port, ILogger? logger = null) + : this(Extensions.CreateDnsEndPoint(host, port), logger) { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - string[] values = host.Split(':'); - - DnsEndPoint endPoint = values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port); - - socket = new TcpSocket(); - socket.Connect(endPoint); - socket.ReceiveBufferSize = ReceiveBufferSize; -#if HAS_LOGGER - this.logger = logger ?? NullLogger.Instance; -#endif } -#if !HAS_LOGGER -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// /// Initializes a new instance of the class. /// /// The at which the Android Debug Bridge is listening for clients. - public AdbSocket(ITcpSocket socket) + /// The logger to use when logging. + public AdbSocket(ITcpSocket socket, ILogger? logger = null) { - this.socket = socket; -#if HAS_LOGGER - logger ??= NullLogger.Instance; -#endif + Socket = socket; + this.logger = logger ?? LoggerProvider.CreateLogger(); } /// @@ -116,41 +73,62 @@ public AdbSocket(ITcpSocket socket) /// public static int WriteBufferSize { get; set; } = 1024; + /// + /// The underlying TCP socket that manages the connection with the ADB server. + /// + public ITcpSocket Socket { get; init; } + /// /// Determines whether the specified reply is okay. /// /// The reply. /// if the specified reply is okay; otherwise, . +#if HAS_BUFFERS + public static bool IsOkay(ReadOnlySpan reply) => AdbClient.Encoding.GetString(reply).Equals("OKAY"); +#else public static bool IsOkay(byte[] reply) => AdbClient.Encoding.GetString(reply).Equals("OKAY"); +#endif /// - public bool Connected => socket.Connected; + public bool Connected => Socket.Connected; /// - public virtual void Reconnect() => socket.Reconnect(); + public virtual void Reconnect(bool isForce = false) => Socket.Reconnect(isForce); /// - public void Send(byte[] data, int length) => Send(data, 0, length); + public virtual void Send(byte[] data, int length) + { + try + { + int count = Socket.Send(data, length != -1 ? length : data.Length, SocketFlags.None); + + if (count < 0) + { + throw new AdbException("channel EOF"); + } + } + catch (SocketException sex) + { + logger.LogError(sex, sex.Message); + throw; + } + } /// public virtual void Send(byte[] data, int offset, int length) { try { - int count = socket.Send(data, 0, length != -1 ? length : data.Length, SocketFlags.None); + int count = Socket.Send(data, offset, length != -1 ? length : data.Length, SocketFlags.None); + if (count < 0) { throw new AdbException("channel EOF"); } } -#if HAS_LOGGER catch (SocketException sex) { logger.LogError(sex, sex.Message); -#else - catch (SocketException) - { -#endif throw; } } @@ -202,46 +180,46 @@ public virtual void SendAdbRequest(string request) } /// - public int Read(byte[] data) => Read(data, data.Length); + public virtual int Read(byte[] data, int length) => Read(data, 0, length); /// - public virtual int Read(byte[] data, int length) + public virtual int Read(byte[] data, int offset, int length) { - int expLen = length != -1 ? length : data.Length; + ExceptionExtensions.ThrowIfNull(data); + ExceptionExtensions.ThrowIfNegative(offset); + + length = length != -1 ? length : data.Length; + ExceptionExtensions.ThrowIfLessThan(data.Length, length, nameof(data)); + int count = -1; - int totalRead = 0; + int totalRead = offset; - while (count != 0 && totalRead < expLen) + while (count != 0 && totalRead < length) { try { - int left = expLen - totalRead; + int left = length - totalRead; int bufferLength = left < ReceiveBufferSize ? left : ReceiveBufferSize; - byte[] buffer = new byte[bufferLength]; - count = socket.Receive(buffer, bufferLength, SocketFlags.None); + count = Socket.Receive(data, totalRead, bufferLength, SocketFlags.None); + if (count < 0) { -#if HAS_LOGGER logger.LogError("read: channel EOF"); -#endif throw new AdbException("EOF"); } else if (count == 0) { -#if HAS_LOGGER logger.LogInformation("DONE with Read"); -#endif } else { - Array.Copy(buffer, 0, data, totalRead, count); totalRead += count; } } - catch (SocketException sex) + catch (SocketException ex) { - throw new AdbException($"No Data to read: {sex.Message}"); + throw new AdbException($"An error occurred while receiving data from the adb server: {ex.Message}.", ex); } } @@ -258,7 +236,7 @@ public virtual string ReadString() if (read == 0) { // There is no data to read - return null; + return string.Empty; } // Convert the bytes to a hex string @@ -285,7 +263,11 @@ public virtual string ReadSyncString() Array.Reverse(reply); } +#if HAS_BUFFERS + int len = BitConverter.ToInt32(reply); +#else int len = BitConverter.ToInt32(reply, 0); +#endif // And get the string reply = new byte[len]; @@ -300,7 +282,6 @@ public virtual SyncCommand ReadSyncResponse() { byte[] data = new byte[4]; _ = Read(data); - return SyncCommandConverter.GetCommand(data); } @@ -311,17 +292,97 @@ public virtual AdbResponse ReadAdbResponse() if (!response.IOSuccess || !response.Okay) { - socket.Dispose(); + Socket.Dispose(); throw new AdbException($"An error occurred while reading a response from ADB: {response.Message}", response); } return response; } +#if HAS_BUFFERS + /// + public virtual void Send(ReadOnlySpan data) + { + try + { + int count = Socket.Send(data, SocketFlags.None); + if (count < 0) + { + throw new AdbException("channel EOF"); + } + } + catch (SocketException sex) + { + logger.LogError(sex, sex.Message); + throw; + } + } + + /// + public virtual int Read(Span data) + { + int count = -1; + int totalRead = 0; + int length = data.Length; + + while (count != 0 && totalRead < length) + { + try + { + int left = length - totalRead; + int bufferLength = left < ReceiveBufferSize ? left : ReceiveBufferSize; + + count = Socket.Receive(data.Slice(totalRead, bufferLength), SocketFlags.None); + + if (count < 0) + { + logger.LogError("read: channel EOF"); + throw new AdbException("EOF"); + } + else if (count == 0) + { + logger.LogInformation("DONE with Read"); + } + else + { + totalRead += count; + } + } + catch (SocketException ex) + { + throw new AdbException($"An error occurred while receiving data from the adb server: {ex.Message}.", ex); + } + } + + return totalRead; + } +#else + /// + public virtual void Send(byte[] data) + { + try + { + int count = Socket.Send(data, SocketFlags.None); + if (count < 0) + { + throw new AdbException("channel EOF"); + } + } + catch (SocketException sex) + { + logger.LogError(sex, sex.Message); + throw; + } + } + + /// + public virtual int Read(byte[] data) => Read(data, 0, data.Length); +#endif + /// - public Stream GetShellStream() + public virtual Stream GetShellStream() { - Stream stream = socket.GetStream(); + Stream stream = Socket.GetStream(); return new ShellStream(stream, closeStream: true); } @@ -358,20 +419,19 @@ public void SetDevice(DeviceData device) /// The data to send. /// Returns if all data was written; otherwise, . /// This uses the default time out value. +#if HAS_BUFFERS + protected virtual bool Write(ReadOnlySpan data) +#else protected virtual bool Write(byte[] data) +#endif { try { - Send(data, -1); + Send(data); } -#if HAS_LOGGER catch (IOException e) { logger.LogError(e, e.Message); -#else - catch (IOException) - { -#endif return false; } @@ -384,25 +444,19 @@ protected virtual bool Write(byte[] data) /// A that represents the response received from ADB. protected virtual AdbResponse ReadAdbResponseInner() { - AdbResponse rasps = new(); - byte[] reply = new byte[4]; Read(reply); - rasps.IOSuccess = true; - - rasps.Okay = IsOkay(reply); - - if (!rasps.Okay) + if (IsOkay(reply)) + { + return AdbResponse.OK; + } + else { string message = ReadString(); - rasps.Message = message; -#if HAS_LOGGER - logger.LogError($"Got reply '{ReplyToString(reply)}', diag='{rasps.Message}'"); -#endif + logger.LogError("Got reply '{0}', diag='{1}'", ReplyToString(reply), message); + return AdbResponse.FromError(message); } - - return rasps; } /// @@ -410,24 +464,22 @@ protected virtual AdbResponse ReadAdbResponseInner() /// /// A array that represents the ADB reply. /// A that represents the ADB reply. +#if HAS_BUFFERS + protected virtual string ReplyToString(ReadOnlySpan reply) +#else protected virtual string ReplyToString(byte[] reply) +#endif { string result; try { result = Encoding.ASCII.GetString(reply); } -#if HAS_LOGGER catch (DecoderFallbackException e) { logger.LogError(e, e.Message); -#else - catch (DecoderFallbackException) - { -#endif result = string.Empty; } - return result; } @@ -438,7 +490,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - socket.Dispose(); + Socket.Dispose(); } } @@ -448,5 +500,8 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + + /// + public virtual void Close() => Socket.Dispose(); } } diff --git a/AdvancedSharpAdbClient/AdvancedSharpAdbClient.csproj b/AdvancedSharpAdbClient/AdvancedSharpAdbClient.csproj index 162538f8..b7f76945 100644 --- a/AdvancedSharpAdbClient/AdvancedSharpAdbClient.csproj +++ b/AdvancedSharpAdbClient/AdvancedSharpAdbClient.csproj @@ -3,24 +3,21 @@ False True - True - True - False $(NoWarn);CS1685;CA2254 + Enable - $(NoWarn);NU1603;NU1605 - net6.0;netstandard1.3;netstandard2.0;netstandard2.1 - $(TargetFrameworks);net8.0;netcoreapp2.1;netcoreapp3.1 - $(TargetFrameworks);net2.0-client;net3.5-client;net4.0-client;net4.5.2;net4.6.2;net4.7.2;net4.8.1;net6.0-windows10.0.17763.0;net8.0-windows10.0.17763.0;netcore50;uap10.0;uap10.0.15138.0 + $(NoWarn);NU1603;NU1605;NU1902;NU1903 + net6.0;net8.0;netcoreapp2.1;netcoreapp3.1;netstandard1.3;netstandard2.0;netstandard2.1 + $(TargetFrameworks);net2.0-client;net3.5-client;net4.0-client;net4.5.2;net4.6.2;net4.8.1;net6.0-windows10.0.17763.0;net8.0-windows10.0.17763.0 + $(TargetFrameworks);netcore5.0;uap10.0;uap10.0.15138.0 - netstandard1.3;netstandard2.0;netstandard2.1 - $(TargetFrameworks);net6.0;net8.0;netcoreapp3.1 - $(TargetFrameworks);net2.0-client;net3.5-client;net4.5.2;net4.8.1 + net8.0;netcoreapp3.1;netstandard1.3;netstandard2.0;netstandard2.1 + $(TargetFrameworks);net2.0-client;net3.5-client;net4.5.2;net4.8.1;net8.0-windows10.0.17763.0 @@ -36,10 +33,11 @@ - + - + @@ -51,11 +49,11 @@ - + - - - - - - - + + - - + + $(DefineConstants);HAS_TASK - - $(DefineConstants);HAS_BUFFERS - - - - $(DefineConstants);HAS_LOGGER - - - $(DefineConstants);HAS_DRAWING - - - - $(DefineConstants);HAS_DRAWING + or '$(TargetFramework)' == 'net8.0-windows10.0.17763.0'"> + $(DefineConstants);HAS_IMAGING - $(DefineConstants);HAS_INDEXRANGE + or '$(TargetFramework)' == 'netstandard2.1' + or '$(TargetFramework)' == 'uap10.0.15138.0'"> + $(DefineConstants);HAS_FULLSTRING $(DefineConstants);HAS_RUNTIMEINFORMATION - - $(DefineConstants);HAS_LOGGER;HAS_OLDLOGGER + + $(DefineConstants);HAS_BUFFERS;HAS_INDEXRANGE - - - $(DefineConstants);HAS_PROCESS;HAS_SERIALIZATION + $(DefineConstants);HAS_PROCESS;HAS_DRAWING;HAS_SERIALIZATION diff --git a/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.Async.cs b/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.Async.cs index 2f2a74bf..b61a470b 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.Async.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.Async.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; @@ -15,6 +14,17 @@ namespace AdvancedSharpAdbClient.DeviceCommands { public static partial class DeviceExtensions { + /// + /// Executes a shell command on the device. + /// + /// The to use when executing the command. + /// The device on which to run the command. + /// The command to execute. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteShellCommandAsync(this IAdbClient client, DeviceData device, string command, CancellationToken cancellationToken = default) => + client.ExecuteRemoteCommandAsync(command, device, AdbClient.Encoding, cancellationToken); + /// /// Executes a shell command on the device. /// @@ -25,7 +35,7 @@ public static partial class DeviceExtensions /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. public static Task ExecuteShellCommandAsync(this IAdbClient client, DeviceData device, string command, IShellOutputReceiver receiver, CancellationToken cancellationToken = default) => - client.ExecuteRemoteCommandAsync(command, device, receiver, cancellationToken); + client.ExecuteRemoteCommandAsync(command, device, receiver, AdbClient.Encoding, cancellationToken); /// /// Gets the file statistics of a given file. @@ -38,7 +48,7 @@ public static Task ExecuteShellCommandAsync(this IAdbClient client, DeviceData d public static async Task StatAsync(this IAdbClient client, DeviceData device, string path, CancellationToken cancellationToken = default) { using ISyncService service = Factories.SyncServiceFactory(client, device); - return await service.StatAsync(path, cancellationToken); + return await service.StatAsync(path, cancellationToken).ConfigureAwait(false); } /// @@ -49,10 +59,10 @@ public static async Task StatAsync(this IAdbClient client, Devic /// The path to the directory on the device. /// A that can be used to cancel the task. /// A which return for each child item of the directory, a object with information of the item. - public static async Task> List(this IAdbClient client, DeviceData device, string remotePath, CancellationToken cancellationToken = default) + public static async Task> ListAsync(this IAdbClient client, DeviceData device, string remotePath, CancellationToken cancellationToken = default) { using ISyncService service = Factories.SyncServiceFactory(client, device); - return await service.GetDirectoryListingAsync(remotePath, cancellationToken); + return await service.GetDirectoryListingAsync(remotePath, cancellationToken).ConfigureAwait(false); } /// @@ -68,16 +78,17 @@ public static async Task> List(this IAdbClient clien /// A which represents the asynchronous operation. public static async Task PullAsync(this IAdbClient client, DeviceData device, string remotePath, Stream stream, - EventHandler syncProgressEventHandler = null, - IProgress progress = null, CancellationToken cancellationToken = default ) + EventHandler? syncProgressEventHandler = null, + IProgress? progress = null, CancellationToken cancellationToken = default) { using ISyncService service = Factories.SyncServiceFactory(client, device); if (syncProgressEventHandler != null) { + service.SyncProgressChanged -= syncProgressEventHandler; service.SyncProgressChanged += syncProgressEventHandler; } - - await service.PullAsync(remotePath, stream, progress, cancellationToken); + await service.PullAsync(remotePath, stream, progress, cancellationToken).ConfigureAwait(false); + service.SyncProgressChanged -= syncProgressEventHandler; } /// @@ -95,16 +106,17 @@ public static async Task PullAsync(this IAdbClient client, DeviceData device, /// A which represents the asynchronous operation. public static async Task PushAsync(this IAdbClient client, DeviceData device, string remotePath, Stream stream, int permissions, DateTimeOffset timestamp, - EventHandler syncProgressEventHandler = null, - IProgress progress = null, CancellationToken cancellationToken = default ) + EventHandler? syncProgressEventHandler = null, + IProgress? progress = null, CancellationToken cancellationToken = default) { using ISyncService service = Factories.SyncServiceFactory(client, device); if (syncProgressEventHandler != null) { + service.SyncProgressChanged -= syncProgressEventHandler; service.SyncProgressChanged += syncProgressEventHandler; } - - await service.PushAsync(stream, remotePath, permissions, timestamp, progress, cancellationToken); + await service.PushAsync(stream, remotePath, permissions, timestamp, progress, cancellationToken).ConfigureAwait(false); + service.SyncProgressChanged -= syncProgressEventHandler; } /// @@ -118,7 +130,7 @@ public static async Task PushAsync(this IAdbClient client, DeviceData device, public static async Task GetPropertyAsync(this IAdbClient client, DeviceData device, string property, CancellationToken cancellationToken = default) { ConsoleOutputReceiver receiver = new(); - await client.ExecuteRemoteCommandAsync($"{GetPropReceiver.GetPropCommand} {property}", device, receiver, cancellationToken); + await client.ExecuteShellCommandAsync(device, $"{GetPropReceiver.GetPropCommand} {property}", receiver, cancellationToken).ConfigureAwait(false); return receiver.ToString(); } @@ -132,7 +144,7 @@ public static async Task GetPropertyAsync(this IAdbClient client, Device public static async Task> GetPropertiesAsync(this IAdbClient client, DeviceData device, CancellationToken cancellationToken = default) { GetPropReceiver receiver = new(); - await client.ExecuteRemoteCommandAsync(GetPropReceiver.GetPropCommand, device, receiver, cancellationToken); + await client.ExecuteShellCommandAsync(device, GetPropReceiver.GetPropCommand, receiver, cancellationToken).ConfigureAwait(false); return receiver.Properties; } @@ -146,7 +158,7 @@ public static async Task> GetPropertiesAsync(this IAd public static async Task> GetEnvironmentVariablesAsync(this IAdbClient client, DeviceData device, CancellationToken cancellationToken = default) { EnvironmentVariablesReceiver receiver = new(); - await client.ExecuteRemoteCommandAsync(EnvironmentVariablesReceiver.PrintEnvCommand, device, receiver, cancellationToken); + await client.ExecuteShellCommandAsync(device, EnvironmentVariablesReceiver.PrintEnvCommand, receiver, cancellationToken).ConfigureAwait(false); return receiver.EnvironmentVariables; } @@ -200,7 +212,7 @@ public static async Task> ListProcessesAsync(this IA // The easiest way to do the directory listings would be to use the SyncService; unfortunately, // the sync service doesn't work very well with /proc/ so we're back to using ls and taking it // from there. - List processes = new(); + List processes = []; // List all processes by doing ls /proc/. // All subfolders which are completely numeric are PIDs @@ -222,18 +234,18 @@ await client.ExecuteShellCommandAsync(device, @"SDK=""$(/system/bin/getprop ro.b /system/bin/ls /proc/ else /system/bin/ls -1 /proc/ -fi".Replace("\r\n", "\n"), receiver, cancellationToken); +fi".Replace("\r\n", "\n"), receiver, cancellationToken).ConfigureAwait(false); - Collection pids = new(); + List pids = []; string output = receiver.ToString(); using (StringReader reader = new(output)) { while (reader.Peek() > 0) { - string line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + string? line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (!line.All(char.IsDigit)) + if (line?.All(char.IsDigit) != true) { continue; } @@ -252,17 +264,16 @@ await client.ExecuteShellCommandAsync(device, @"SDK=""$(/system/bin/getprop ro.b StringBuilder catBuilder = new(); ProcessOutputReceiver processOutputReceiver = new(); - _ = catBuilder.Append("cat "); + _ = catBuilder.Append("cat"); for (int i = 0; i < pids.Count; i++) { - _ = catBuilder.Append($"/proc/{pids[i]}/cmdline /proc/{pids[i]}/stat "); + _ = catBuilder.AppendFormat(" /proc/{0}/cmdline /proc/{1}/stat", pids[i], pids[i]); if (i > 0 && (i % 25 == 0 || i == pids.Count - 1)) { - await client.ExecuteShellCommandAsync(device, catBuilder.ToString(), processOutputReceiver, cancellationToken); - _ = catBuilder.Clear(); - _ = catBuilder.Append("cat "); + await client.ExecuteShellCommandAsync(device, catBuilder.ToString(), processOutputReceiver, cancellationToken).ConfigureAwait(false); + _ = catBuilder.Clear().Append("cat"); } } diff --git a/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.cs b/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.cs index e1d9f2e1..5799897e 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/DeviceExtensions.cs @@ -4,11 +4,9 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; -using System.Threading; namespace AdvancedSharpAdbClient.DeviceCommands { @@ -18,6 +16,15 @@ namespace AdvancedSharpAdbClient.DeviceCommands /// public static partial class DeviceExtensions { + /// + /// Executes a shell command on the device. + /// + /// The to use when executing the command. + /// The device on which to run the command. + /// The command to execute. + public static void ExecuteShellCommand(this IAdbClient client, DeviceData device, string command) => + client.ExecuteRemoteCommand(command, device, AdbClient.Encoding); + /// /// Executes a shell command on the device. /// @@ -26,7 +33,7 @@ public static partial class DeviceExtensions /// The command to execute. /// Optionally, a that processes the command output. public static void ExecuteShellCommand(this IAdbClient client, DeviceData device, string command, IShellOutputReceiver receiver) => - client.ExecuteRemoteCommand(command, device, receiver); + client.ExecuteRemoteCommand(command, device, receiver, AdbClient.Encoding); /// /// Gets the file statistics of a given file. @@ -51,13 +58,12 @@ public static FileStatistics Stat(this IAdbClient client, DeviceData device, str public static IEnumerable List(this IAdbClient client, DeviceData device, string remotePath) { using ISyncService service = Factories.SyncServiceFactory(client, device); - return service.GetDirectoryListing(remotePath); + foreach (FileStatistics fileStatistics in service.GetDirectoryListing(remotePath)) + { + yield return fileStatistics; + } } -#if !HAS_TASK -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#pragma warning disable CS1574 // XML 注释中有未能解析的 cref 特性 -#endif /// /// Pulls (downloads) a file from the remote device. /// @@ -67,27 +73,21 @@ public static IEnumerable List(this IAdbClient client, DeviceDat /// A that will receive the contents of the file. /// An optional handler for the event. /// An optional parameter which, when specified, returns progress notifications. The progress is reported as a value between 0 and 100, representing the percentage of the file which has been transferred. - /// A that can be used to cancel the task. + /// A that can be used to cancel the task. public static void Pull(this IAdbClient client, DeviceData device, string remotePath, Stream stream, - EventHandler syncProgressEventHandler = null, - IProgress progress = null -#if HAS_TASK - , CancellationToken cancellationToken = default -#endif - ) + EventHandler? syncProgressEventHandler = null, + IProgress? progress = null, + in bool isCancelled = false) { using ISyncService service = Factories.SyncServiceFactory(client, device); if (syncProgressEventHandler != null) { + service.SyncProgressChanged -= syncProgressEventHandler; service.SyncProgressChanged += syncProgressEventHandler; } - - service.Pull(remotePath, stream, progress -#if HAS_TASK - , cancellationToken -#endif - ); + service.Pull(remotePath, stream, progress, in isCancelled); + service.SyncProgressChanged -= syncProgressEventHandler; } /// @@ -101,32 +101,22 @@ public static void Pull(this IAdbClient client, DeviceData device, /// The time at which the file was last modified. /// An optional handler for the event. /// An optional parameter which, when specified, returns progress notifications. The progress is reported as a value between 0 and 100, representing the percentage of the file which has been transferred. - /// A that can be used to cancel the task. + /// A that can be used to cancel the task. public static void Push(this IAdbClient client, DeviceData device, string remotePath, Stream stream, int permissions, DateTimeOffset timestamp, - EventHandler syncProgressEventHandler = null, - IProgress progress = null -#if HAS_TASK - , CancellationToken cancellationToken = default -#endif - ) + EventHandler? syncProgressEventHandler = null, + IProgress? progress = null, + in bool isCancelled = false) { using ISyncService service = Factories.SyncServiceFactory(client, device); if (syncProgressEventHandler != null) { + service.SyncProgressChanged -= syncProgressEventHandler; service.SyncProgressChanged += syncProgressEventHandler; } - - service.Push(stream, remotePath, permissions, timestamp, progress -#if HAS_TASK - , cancellationToken -#endif - ); + service.Push(stream, remotePath, permissions, timestamp, progress, in isCancelled); + service.SyncProgressChanged -= syncProgressEventHandler; } -#if !HAS_TASK -#pragma warning restore CS1574 // XML 注释中有未能解析的 cref 特性 -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// /// Gets the property of a device. @@ -138,7 +128,7 @@ public static void Push(this IAdbClient client, DeviceData device, public static string GetProperty(this IAdbClient client, DeviceData device, string property) { ConsoleOutputReceiver receiver = new(); - client.ExecuteRemoteCommand($"{GetPropReceiver.GetPropCommand} {property}", device, receiver); + client.ExecuteShellCommand(device, $"{GetPropReceiver.GetPropCommand} {property}", receiver); return receiver.ToString(); } @@ -151,7 +141,7 @@ public static string GetProperty(this IAdbClient client, DeviceData device, stri public static Dictionary GetProperties(this IAdbClient client, DeviceData device) { GetPropReceiver receiver = new(); - client.ExecuteRemoteCommand(GetPropReceiver.GetPropCommand, device, receiver); + client.ExecuteShellCommand(device, GetPropReceiver.GetPropCommand, receiver); return receiver.Properties; } @@ -164,7 +154,7 @@ public static Dictionary GetProperties(this IAdbClient client, D public static Dictionary GetEnvironmentVariables(this IAdbClient client, DeviceData device) { EnvironmentVariablesReceiver receiver = new(); - client.ExecuteRemoteCommand(EnvironmentVariablesReceiver.PrintEnvCommand, device, receiver); + client.ExecuteShellCommand(device, EnvironmentVariablesReceiver.PrintEnvCommand, receiver); return receiver.EnvironmentVariables; } @@ -213,7 +203,7 @@ public static IEnumerable ListProcesses(this IAdbClient client, // The easiest way to do the directory listings would be to use the SyncService; unfortunately, // the sync service doesn't work very well with /proc/ so we're back to using ls and taking it // from there. - List processes = new(); + List processes = []; // List all processes by doing ls /proc/. // All subfolders which are completely numeric are PIDs @@ -237,16 +227,16 @@ public static IEnumerable ListProcesses(this IAdbClient client, /system/bin/ls -1 /proc/ fi".Replace("\r\n", "\n"), receiver); - Collection pids = new(); + List pids = []; string output = receiver.ToString(); using (StringReader reader = new(output)) { while (reader.Peek() > 0) { - string line = reader.ReadLine(); + string? line = reader.ReadLine(); - if (!line.All(char.IsDigit)) + if (line?.All(char.IsDigit) != true) { continue; } @@ -265,17 +255,16 @@ public static IEnumerable ListProcesses(this IAdbClient client, StringBuilder catBuilder = new(); ProcessOutputReceiver processOutputReceiver = new(); - _ = catBuilder.Append("cat "); + _ = catBuilder.Append("cat"); for (int i = 0; i < pids.Count; i++) { - _ = catBuilder.Append($"/proc/{pids[i]}/cmdline /proc/{pids[i]}/stat "); + _ = catBuilder.AppendFormat(" /proc/{0}/cmdline /proc/{1}/stat", pids[i], pids[i]); if (i > 0 && (i % 25 == 0 || i == pids.Count - 1)) { client.ExecuteShellCommand(device, catBuilder.ToString(), processOutputReceiver); - catBuilder.Clear(); - _ = catBuilder.Append("cat "); + _ = catBuilder.Clear().Append("cat"); } } diff --git a/AdvancedSharpAdbClient/DeviceCommands/LinuxPath.cs b/AdvancedSharpAdbClient/DeviceCommands/LinuxPath.cs index 9f3cf83a..4a6a0066 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/LinuxPath.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/LinuxPath.cs @@ -2,8 +2,8 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; @@ -26,10 +26,7 @@ public static partial class LinuxPath /// private const string EscapePattern = "([\\\\()*+?\"'#/\\s])"; - private static readonly char[] InvalidCharacters = new char[] - { - '|', '\\', '?', '*', '<', '\"', ':', '>' - }; + private static readonly char[] InvalidCharacters = ['|', '\\', '?', '*', '<', '\"', ':', '>']; /// /// Combine the specified paths to form one path. @@ -114,7 +111,8 @@ public static string Combine(params string[] paths) /// The path parameter is longer /// than the system-defined maximum length. /// 1 - public static string GetDirectoryName(string path) + [return: NotNullIfNotNull(nameof(path))] + public static string? GetDirectoryName(string? path) { if (path != null) { @@ -123,11 +121,7 @@ public static string GetDirectoryName(string path) string tpath = path; if (tpath.Length > 1) { -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (tpath.EndsWith(DirectorySeparatorChar)) -#else - if (tpath.EndsWith(new string(new char[] { DirectorySeparatorChar }))) -#endif { return tpath; } @@ -140,14 +134,16 @@ public static string GetDirectoryName(string path) } else if (tpath.Length == 1) { - return new string(new char[] { DirectorySeparatorChar }); + return new string([DirectorySeparatorChar]); } } return null; } - /// Returns the file name and extension of the specified path string. + /// + /// Returns the file name and extension of the specified path string. + /// /// A consisting of the characters after the last directory character in path. /// If the last character of path is a directory or volume separator character, /// this method returns . If path is null, this method returns null. @@ -155,7 +151,8 @@ public static string GetDirectoryName(string path) /// path contains one or more of the invalid characters /// defined in , or contains a wildcard character. /// 1 - public static string GetFileName(string path) + [return: NotNullIfNotNull(nameof(path))] + public static string? GetFileName(string path) { if (path != null) { @@ -190,7 +187,7 @@ public static bool IsPathRooted(string path) CheckInvalidPathChars(path); int length = path.Length; if ((length >= 1 && (path[0] == DirectorySeparatorChar)) || - (length == 1)) + length == 1) { return true; } @@ -234,28 +231,19 @@ internal static void CheckInvalidPathChars(string path) /// The fixup path private static string FixupPath(string path) { - string sb = path; - sb = sb.Replace(Path.DirectorySeparatorChar, DirectorySeparatorChar); + string sb = path.Replace(Path.DirectorySeparatorChar, DirectorySeparatorChar); -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (sb != "." && !sb.StartsWith(DirectorySeparatorChar)) -#else - if (sb != "." && !sb.StartsWith(new string(new char[] { DirectorySeparatorChar }))) -#endif { sb = $".{DirectorySeparatorChar}{sb}"; } -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (!sb.EndsWith(DirectorySeparatorChar)) -#else - if (!sb.EndsWith(new string(new char[] { DirectorySeparatorChar }))) -#endif { sb = $"{sb}{DirectorySeparatorChar}"; } - sb = sb.Replace("//", new string(new char[] { DirectorySeparatorChar })); + sb = sb.Replace("//", new string([DirectorySeparatorChar])); return sb; } diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/AndroidProcess.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/AndroidProcess.cs index c7dcc21d..7bd1dd7f 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Models/AndroidProcess.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/AndroidProcess.cs @@ -2,45 +2,326 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Models.DeviceCommands { /// /// Represents a process running on an Android device. /// - public class AndroidProcess + public readonly struct AndroidProcess { + /// + /// Initializes a new instance of the struct. + /// + public AndroidProcess() { } + + /// + /// Initializes a new instance of the struct from it representation. + /// + /// A which represents a . + /// A value indicating whether the output of /proc/{pid}/stat is prefixed with /proc/{pid}/cmdline or not. + /// Because stat does not contain the full process name, this can be useful. + public AndroidProcess(string line, bool cmdLinePrefix = false) + { + ExceptionExtensions.ThrowIfNull(line); + + // See http://man7.org/linux/man-pages/man5/proc.5.html, + // section /proc/[pid]/stat, for more information about the file format + + // Space delimited, so normally we would just do a string.split + // The process name may contain spaces but is wrapped within parentheses, all other values (we know of) are + // numeric. + // So we parse the pid & process name manually, to account for this, and do the string.split afterwards :-) + int processNameStart = line.IndexOf('('); + int processNameEnd = line.LastIndexOf(')'); + + int pid; + string comm; + + bool parsedCmdLinePrefix = false; + + if (cmdLinePrefix) + { +#if HAS_INDEXRANGE + string[] cmdLineParts = line[..processNameStart] +#else + + string[] cmdLineParts = line.Substring(0, processNameStart) +#endif + .Split('\0'); + + if (cmdLineParts.Length <= 1) + { + parsedCmdLinePrefix = false; + } + else + { +#if HAS_INDEXRANGE + pid = int.Parse(cmdLineParts[^1]); +#else + pid = int.Parse(cmdLineParts[cmdLineParts.Length - 1]); +#endif + ProcessId = pid; + + comm = cmdLineParts[0]; + Name = comm; + + // All the other parts are the command line arguments, skip them. + parsedCmdLinePrefix = true; + } + } + + if (!parsedCmdLinePrefix) + { +#if HAS_INDEXRANGE + pid = int.Parse(line[..processNameStart]); +#else + pid = int.Parse(line.Substring(0, processNameStart)); +#endif + ProcessId = pid; + + comm = line.Substring(processNameStart + 1, processNameEnd - processNameStart - 1); + Name = comm; + } + +#if HAS_INDEXRANGE + string[] parts = line[(processNameEnd + 1)..] +#else + string[] parts = line.Substring(processNameEnd + 1) +#endif + .Split(' ', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 35) + { + throw new ArgumentOutOfRangeException(nameof(line)); + } + + // Only fields in Linux 2.1.10 and earlier are listed here, + // additional fields exist in newer versions of Linux. + string state = parts[0]; + State = (AndroidProcessState)Enum.Parse(typeof(AndroidProcessState), state, true); + + int ppid = ParseInt(parts[1]); + ParentProcessId = ppid; + + int pgrp = ParseInt(parts[2]); + ProcessGroupId = pgrp; + + int session = ParseInt(parts[3]); + SessionID = session; + + int tty_nr = ParseInt(parts[4]); + TTYNumber = tty_nr; + + int tpgid = ParseInt(parts[5]); + TopProcessGroupId = tpgid; + + uint flags = ParseUInt(parts[6]); + Flags = (PerProcessFlags)flags; + + ulong minflt = ParseULong(parts[7]); + MinorFaults = minflt; + + ulong cminflt = ParseULong(parts[8]); + ChildMinorFaults = cminflt; + + ulong majflt = ParseULong(parts[9]); + MajorFaults = majflt; + + ulong cmajflt = ParseULong(parts[10]); + ChildMajorFaults = cmajflt; + + ulong utime = ParseULong(parts[11]); + UserScheduledTime = utime; + + ulong stime = ParseULong(parts[12]); + ScheduledTime = stime; + + long cutime = ParseLong(parts[13]); + ChildUserScheduledTime = cutime; + + long cstime = ParseLong(parts[14]); + ChildScheduledTime = cstime; + + long priority = ParseLong(parts[15]); + Priority = priority; + + long nice = ParseLong(parts[16]); + Nice = nice; + + long num_threads = ParseLong(parts[17]); + ThreadsNumber = num_threads; + + long itrealvalue = ParseLong(parts[18]); + Interval = itrealvalue; + + ulong starttime = ParseULong(parts[19]); + StartTime = starttime; + + ulong vsize = ParseULong(parts[20]); + VirtualSize = vsize; + + int rss = int.Parse(parts[21]); + ResidentSetSize = rss; + + ulong rsslim = ParseULong(parts[22]); + ResidentSetSizeLimit = rsslim; + + ulong startcode = ParseULong(parts[23]); + StartCode = startcode; + + ulong endcode = ParseULong(parts[24]); + EndCode = endcode; + + ulong startstack = ParseULong(parts[25]); + StartStack = startstack; + + ulong kstkesp = ParseULong(parts[26]); + ESP = kstkesp; + + ulong kstkeip = ParseULong(parts[27]); + EIP = kstkeip; + + ulong signal = ParseULong(parts[28]); + Signal = signal; + + ulong blocked = ParseULong(parts[29]); + Blocked = blocked; + + ulong sigignore = ParseULong(parts[30]); + IgnoredSignals = sigignore; + + ulong sigcatch = ParseULong(parts[31]); + CaughtSignals = sigcatch; + + ulong wchan = ParseULong(parts[32]); + WChan = wchan; + + ulong nswap = ParseULong(parts[33]); + SwappedPagesNumber = nswap; + + ulong cnswap = ParseULong(parts[34]); + CumulativeSwappedPagesNumber = cnswap; + + if (parts.Length < 36) + { + return; + } + + // Linux 2.1.22 + int exit_signal = ParseInt(parts[35]); + ExitSignal = exit_signal; + + if (parts.Length < 37) + { + return; + } + + // Linux 2.2.8 + int processor = ParseInt(parts[36]); + Processor = processor; + + if (parts.Length < 39) + { + return; + } + + // Linux 2.5.19 + uint rt_priority = ParseUInt(parts[37]); + RealTimePriority = rt_priority; + + uint policy = ParseUInt(parts[38]); + Policy = policy; + + if (parts.Length < 40) + { + return; + } + + // Linux 2.6.18 + ulong delayacct_blkio_ticks = ParseULong(parts[39]); + + IODelays = delayacct_blkio_ticks; + + if (parts.Length < 42) + { + return; + } + + // Linux 2.6.24 + ulong guest_time = ParseULong(parts[40]); + GuestTime = guest_time; + + long cguest_time = ParseLong(parts[41]); + ChildGuestTime = cguest_time; + + if (parts.Length < 45) + { + return; + } + + // Linux 3.3 + ulong start_data = ParseULong(parts[42]); + StartData = start_data; + + ulong end_data = ParseULong(parts[43]); + EndData = end_data; + + ulong start_brk = ParseULong(parts[44]); + StartBrk = start_brk; + + if (parts.Length < 50) + { + return; + } + + // Linux 3.5 + ulong arg_start = ParseULong(parts[45]); + ArgStart = arg_start; + + ulong arg_end = ParseULong(parts[46]); + ArgEnd = arg_end; + + ulong env_start = ParseULong(parts[47]); + EnvStart = env_start; + + ulong env_end = ParseULong(parts[48]); + EnvEnd = env_end; + + int exit_code = ParseInt(parts[49]); + ExitCode = exit_code; + } + /// /// Gets or sets the state of the process. /// - public AndroidProcessState State { get; set; } + public AndroidProcessState State { get; init; } /// /// Gets or sets the name of the process, including arguments, if any. /// - public string Name { get; set; } + public string Name { get; init; } = string.Empty; /// /// Gets or sets the parent Process ID number. /// - public int ParentProcessId { get; set; } + public int ParentProcessId { get; init; } /// /// Gets or sets the Process Group ID number. /// - public int ProcessGroupId { get; set; } + public int ProcessGroupId { get; init; } /// /// Gets or sets the session ID of the process number. /// - public int SessionID { get; set; } + public int SessionID { get; init; } /// /// Gets or sets the Process ID number. /// - public int ProcessId { get; set; } + public int ProcessId { get; init; } /// /// Gets or sets the controlling terminal of the process. @@ -48,40 +329,40 @@ public class AndroidProcess /// The minor device number is contained in the combination of bits /// to and to ; /// the major device number is in bits to . - public int TTYNumber { get; set; } + public int TTYNumber { get; init; } /// /// Gets or sets the ID of the foreground process group of the controlling terminal of the process. /// - public int TopProcessGroupId { get; set; } + public int TopProcessGroupId { get; init; } /// /// Gets or sets The kernel flags word of the process. For bit meanings, see the PF_* defines /// in the Linux kernel source file include/linux/sched.h. Details depend on the kernel version. /// - public PerProcessFlags Flags { get; set; } + public PerProcessFlags Flags { get; init; } /// /// Gets or sets the number of minor faults the process has made /// which have not required loading a memory page from disk. /// - public ulong MinorFaults { get; set; } + public ulong MinorFaults { get; init; } /// /// Gets or sets the number of minor faults that the process's waited-for children have made. /// - public ulong ChildMinorFaults { get; set; } + public ulong ChildMinorFaults { get; init; } /// /// Gets or sets the number of major faults the process has made /// which have required loading a memory page from disk. /// - public ulong MajorFaults { get; set; } + public ulong MajorFaults { get; init; } /// /// Gets or sets the number of major faults that the process's waited-for children have made. /// - public ulong ChildMajorFaults { get; set; } + public ulong ChildMajorFaults { get; init; } /// /// Gets or sets the amount of time that this process has been scheduled in user mode, @@ -89,26 +370,26 @@ public class AndroidProcess /// guest_time (time spent running a virtual CPU, see below), so that applications /// that are not aware of the guest time field do not lose that time from their calculations. /// - public ulong UserScheduledTime { get; set; } + public ulong UserScheduledTime { get; init; } /// /// Gets or sets the amount of time that this process has been scheduled in kernel mode, /// measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). /// - public ulong ScheduledTime { get; set; } + public ulong ScheduledTime { get; init; } /// /// Gets or sets the amount of time that this process's waited-for children have been scheduled in user mode, /// measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). (See also times(2).) /// This includes guest time, cguest_time (time spent running a virtual CPU, see below). /// - public long ChildUserScheduledTime { get; set; } + public long ChildUserScheduledTime { get; init; } /// /// Gets or sets the Amount of time that this process's waited-for children have been scheduled in kernel mode, /// measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). /// - public long ChildScheduledTime { get; set; } + public long ChildScheduledTime { get; init; } /// /// Gets or sets the value for processes running a real-time scheduling policy @@ -120,25 +401,25 @@ public class AndroidProcess /// in the range (high) to (low), /// corresponding to the user-visible nice range of to . /// - public long Priority { get; set; } + public long Priority { get; init; } /// /// Gets or sets the nice value (see setpriority(2)), a value in the range /// (low priority) to (high priority). /// - public long Nice { get; set; } + public long Nice { get; init; } /// /// Gets or sets the number of threads in this process (since Linux 2.6). /// /// Before kernel 2.6, this field was hard coded to 0 as a placeholder for an earlier removed field. - public long ThreadsNumber { get; set; } + public long ThreadsNumber { get; init; } /// /// Gets or sets the time in jiffies before the next SIGALRM is sent to the process due to an interval timer. /// /// Since kernel 2.6.17, this field is no longer maintained, and is hard coded as 0. - public long Interval { get; set; } + public long Interval { get; init; } /// /// Gets or sets The time the process started after system boot. In kernels before Linux 2.6, @@ -146,12 +427,12 @@ public class AndroidProcess /// (divide by sysconf(_SC_CLK_TCK)). /// /// The format for this field was %lu before Linux 2.6. - public ulong StartTime { get; set; } + public ulong StartTime { get; init; } /// /// Gets or sets total virtual memory size in bytes. /// - public ulong VirtualSize { get; set; } + public ulong VirtualSize { get; init; } /// /// Gets or sets the process resident set size. @@ -160,62 +441,62 @@ public class AndroidProcess /// This is just the pages which count toward text, data, or stack space. /// This does not include pages which have not been demand-loaded in, or which are swapped out. /// This value is inaccurate; see /proc/[pid]/statm below. - public int ResidentSetSize { get; set; } + public int ResidentSetSize { get; init; } /// /// Gets or sets current soft limit in bytes on the rss of the process; /// See the description of RLIMIT_RSS in getrlimit(2). /// - public ulong ResidentSetSizeLimit { get; set; } + public ulong ResidentSetSizeLimit { get; init; } /// /// Gets or sets the address above which program text can run. /// - public ulong StartCode { get; set; } + public ulong StartCode { get; init; } /// /// Gets or sets the address below which program text can run. /// - public ulong EndCode { get; set; } + public ulong EndCode { get; init; } /// /// Gets or sets the address of the start (i.e., bottom) of the stack. /// - public ulong StartStack { get; set; } + public ulong StartStack { get; init; } /// /// Gets or sets the current value of ESP (stack pointer), as found in the kernel stack page for the process. /// - public ulong ESP { get; set; } + public ulong ESP { get; init; } /// /// Gets or sets the current EIP (instruction pointer). /// - public ulong EIP { get; set; } + public ulong EIP { get; init; } /// /// Gets or sets the bitmap of pending signals, displayed as a decimal number. Obsolete, /// because it does not provide information on real-time signals; Use /proc/[pid]/status instead. /// - public ulong Signal { get; set; } + public ulong Signal { get; init; } /// /// Gets or sets the bitmap of blocked signals, displayed as a decimal number. Obsolete, /// because it does not provide information on real-time signals; Use /proc/[pid]/status instead. /// - public ulong Blocked { get; set; } + public ulong Blocked { get; init; } /// /// Gets or sets the bitmap of ignored signals, displayed as a decimal number. Obsolete, /// because it does not provide information on real-time signals; Use /proc/[pid]/status instead. /// - public ulong IgnoredSignals { get; set; } + public ulong IgnoredSignals { get; init; } /// /// Gets or sets the bitmap of caught signals, displayed as a decimal number. Obsolete, /// because it does not provide information on real-time signals; Use /proc/[pid]/status instead. /// - public ulong CaughtSignals { get; set; } + public ulong CaughtSignals { get; init; } /// /// Gets or sets the memory address of the event the process is waiting for. @@ -223,95 +504,95 @@ public class AndroidProcess /// This is the "channel" in which the process is waiting. /// It is the address of a location in the kernel where the process is sleeping. /// The corresponding symbolic name can be found in /proc/[pid]/wchan. - public ulong WChan { get; set; } + public ulong WChan { get; init; } /// /// Gets or sets the number of pages swapped (not maintained). /// - public ulong SwappedPagesNumber { get; set; } + public ulong SwappedPagesNumber { get; init; } /// /// Gets or sets the cumulative number of pages swapped for child processes (not maintained). /// - public ulong CumulativeSwappedPagesNumber { get; set; } + public ulong CumulativeSwappedPagesNumber { get; init; } /// /// Gets or sets the signal to be sent to parent when we die. /// - public int ExitSignal { get; set; } + public int ExitSignal { get; init; } /// /// Gets or sets the CPU number last executed on. /// - public int Processor { get; set; } + public int Processor { get; init; } /// /// Gets or sets the real-time scheduling priority, a number in the range 1 to 99 for processes scheduled /// under a real-time policy, or 0, for non-real-time processes (see sched_setscheduler(2)). /// - public uint RealTimePriority { get; set; } + public uint RealTimePriority { get; init; } /// /// Gets or sets the scheduling policy (see sched_setscheduler(2)). /// Decode using the SCHED_* constants in linux/sched.h. /// - public uint Policy { get; set; } + public uint Policy { get; init; } /// /// Gets or sets the aggregated block I/O delays, measured in clock ticks (centiseconds). /// - public ulong IODelays { get; set; } + public ulong IODelays { get; init; } /// /// Gets or sets the guest time of the process (time spent running a virtual CPU for a guest operating system), /// measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). /// - public ulong GuestTime { get; set; } + public ulong GuestTime { get; init; } /// /// Gets or sets the guest time of the process's children, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). /// - public long ChildGuestTime { get; set; } + public long ChildGuestTime { get; init; } /// /// Gets or sets the address above which program initialized and uninitialized(BSS) data are placed. /// - public ulong StartData { get; set; } + public ulong StartData { get; init; } /// /// Gets or sets the address below which program initialized and uninitialized(BSS) data are placed. /// - public ulong EndData { get; set; } + public ulong EndData { get; init; } /// /// Gets or sets the address above which program heap can be expanded with brk(2). /// - public ulong StartBrk { get; set; } + public ulong StartBrk { get; init; } /// /// Gets or sets the address above which program command-line arguments (argv) are placed. /// - public ulong ArgStart { get; set; } + public ulong ArgStart { get; init; } /// /// Gets or sets the address below program command-line arguments (argv) are placed. /// - public ulong ArgEnd { get; set; } + public ulong ArgEnd { get; init; } /// /// Gets or sets the address above which program environment is placed. /// - public ulong EnvStart { get; set; } + public ulong EnvStart { get; init; } /// /// Gets or sets the address below which program environment is placed. /// - public ulong EnvEnd { get; set; } + public ulong EnvEnd { get; init; } /// /// Gets or sets the thread's exit status in the form reported by waitpid(2). /// - public int ExitCode { get; set; } + public int ExitCode { get; init; } /// /// Creates a from it representation. @@ -320,280 +601,7 @@ public class AndroidProcess /// A value indicating whether the output of /proc/{pid}/stat is prefixed with /proc/{pid}/cmdline or not. /// Because stat does not contain the full process name, this can be useful. /// The equivalent . - public static AndroidProcess Parse(string line, bool cmdLinePrefix = false) - { - ExceptionExtensions.ThrowIfNull(line); - - AndroidProcess process = new(); - - // See http://man7.org/linux/man-pages/man5/proc.5.html, - // section /proc/[pid]/stat, for more information about the file format - - // Space delimited, so normally we would just do a string.split - // The process name may contain spaces but is wrapped within parentheses, all other values (we know of) are - // numeric. - // So we parse the pid & process name manually, to account for this, and do the string.split afterwards :-) - int processNameStart = line.IndexOf('('); - int processNameEnd = line.LastIndexOf(')'); - - int pid; - string comm; - - bool parsedCmdLinePrefix = false; - - if (cmdLinePrefix) - { -#if HAS_INDEXRANGE - string[] cmdLineParts = line[..processNameStart] -#else - - string[] cmdLineParts = line.Substring(0, processNameStart) -#endif - .Split(new char[] { '\0' }); - - if (cmdLineParts.Length <= 1) - { - parsedCmdLinePrefix = false; - } - else - { -#if HAS_INDEXRANGE - pid = int.Parse(cmdLineParts[^1]); -#else - pid = int.Parse(cmdLineParts[cmdLineParts.Length - 1]); -#endif - process.ProcessId = pid; - - comm = cmdLineParts[0]; - process.Name = comm; - - // All the other parts are the command line arguments, skip them. - parsedCmdLinePrefix = true; - } - } - - if (!parsedCmdLinePrefix) - { -#if HAS_INDEXRANGE - pid = int.Parse(line[..processNameStart]); -#else - pid = int.Parse(line.Substring(0, processNameStart)); -#endif - process.ProcessId = pid; - - comm = line.Substring(processNameStart + 1, processNameEnd - processNameStart - 1); - process.Name = comm; - } - -#if HAS_INDEXRANGE - string[] parts = line[(processNameEnd + 1)..] -#else - string[] parts = line.Substring(processNameEnd + 1) -#endif - .Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length < 35) - { - throw new ArgumentOutOfRangeException(nameof(line)); - } - - // Only fields in Linux 2.1.10 and earlier are listed here, - // additional fields exist in newer versions of Linux. - string state = parts[0]; - process.State = (AndroidProcessState)Enum.Parse(typeof(AndroidProcessState), state, true); - - int ppid = ParseInt(parts[1]); - process.ParentProcessId = ppid; - - int pgrp = ParseInt(parts[2]); - process.ProcessGroupId = pgrp; - - int session = ParseInt(parts[3]); - process.SessionID = session; - - int tty_nr = ParseInt(parts[4]); - process.TTYNumber = tty_nr; - - int tpgid = ParseInt(parts[5]); - process.TopProcessGroupId = tpgid; - - uint flags = ParseUInt(parts[6]); - process.Flags = (PerProcessFlags)flags; - - ulong minflt = ParseULong(parts[7]); - process.MinorFaults = minflt; - - ulong cminflt = ParseULong(parts[8]); - process.ChildMinorFaults = cminflt; - - ulong majflt = ParseULong(parts[9]); - process.MajorFaults = majflt; - - ulong cmajflt = ParseULong(parts[10]); - process.ChildMajorFaults = cmajflt; - - ulong utime = ParseULong(parts[11]); - process.UserScheduledTime = utime; - - ulong stime = ParseULong(parts[12]); - process.ScheduledTime = stime; - - long cutime = ParseLong(parts[13]); - process.ChildUserScheduledTime = cutime; - - long cstime = ParseLong(parts[14]); - process.ChildScheduledTime = cstime; - - long priority = ParseLong(parts[15]); - process.Priority = priority; - - long nice = ParseLong(parts[16]); - process.Nice = nice; - - long num_threads = ParseLong(parts[17]); - process.ThreadsNumber = num_threads; - - long itrealvalue = ParseLong(parts[18]); - process.Interval = itrealvalue; - - ulong starttime = ParseULong(parts[19]); - process.StartTime = starttime; - - ulong vsize = ParseULong(parts[20]); - process.VirtualSize = vsize; - - int rss = int.Parse(parts[21]); - process.ResidentSetSize = rss; - - ulong rsslim = ParseULong(parts[22]); - process.ResidentSetSizeLimit = rsslim; - - ulong startcode = ParseULong(parts[23]); - process.StartCode = startcode; - - ulong endcode = ParseULong(parts[24]); - process.EndCode = endcode; - - ulong startstack = ParseULong(parts[25]); - process.StartStack = startstack; - - ulong kstkesp = ParseULong(parts[26]); - process.ESP = kstkesp; - - ulong kstkeip = ParseULong(parts[27]); - process.EIP = kstkeip; - - ulong signal = ParseULong(parts[28]); - process.Signal = signal; - - ulong blocked = ParseULong(parts[29]); - process.Blocked = blocked; - - ulong sigignore = ParseULong(parts[30]); - process.IgnoredSignals = sigignore; - - ulong sigcatch = ParseULong(parts[31]); - process.CaughtSignals = sigcatch; - - ulong wchan = ParseULong(parts[32]); - process.WChan = wchan; - - ulong nswap = ParseULong(parts[33]); - process.SwappedPagesNumber = nswap; - - ulong cnswap = ParseULong(parts[34]); - process.CumulativeSwappedPagesNumber = cnswap; - - if (parts.Length < 36) - { - return process; - } - - // Linux 2.1.22 - int exit_signal = ParseInt(parts[35]); - process.ExitSignal = exit_signal; - - if (parts.Length < 37) - { - return process; - } - - // Linux 2.2.8 - int processor = ParseInt(parts[36]); - process.Processor = processor; - - if (parts.Length < 39) - { - return process; - } - - // Linux 2.5.19 - uint rt_priority = ParseUInt(parts[37]); - process.RealTimePriority = rt_priority; - - uint policy = ParseUInt(parts[38]); - process.Policy = policy; - - if (parts.Length < 40) - { - return process; - } - - // Linux 2.6.18 - ulong delayacct_blkio_ticks = ParseULong(parts[39]); - - process.IODelays = delayacct_blkio_ticks; - - if (parts.Length < 42) - { - return process; - } - - // Linux 2.6.24 - ulong guest_time = ParseULong(parts[40]); - process.GuestTime = guest_time; - - long cguest_time = ParseLong(parts[41]); - process.ChildGuestTime = cguest_time; - - if (parts.Length < 45) - { - return process; - } - - // Linux 3.3 - ulong start_data = ParseULong(parts[42]); - process.StartData = start_data; - - ulong end_data = ParseULong(parts[43]); - process.EndData = end_data; - - ulong start_brk = ParseULong(parts[44]); - process.StartBrk = start_brk; - - if (parts.Length < 50) - { - return process; - } - - // Linux 3.5 - ulong arg_start = ParseULong(parts[45]); - process.ArgStart = arg_start; - - ulong arg_end = ParseULong(parts[46]); - process.ArgEnd = arg_end; - - ulong env_start = ParseULong(parts[47]); - process.EnvStart = env_start; - - ulong env_end = ParseULong(parts[48]); - process.EnvEnd = env_end; - - int exit_code = ParseInt(parts[49]); - process.ExitCode = exit_code; - - return process; - } + public static AndroidProcess Parse(string line, bool cmdLinePrefix = false) => new(line, cmdLinePrefix); /// /// Gets a that represents this , diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/AndroidProcessState.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/AndroidProcessState.cs index 7d5eaf50..5fefe092 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/AndroidProcessState.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/AndroidProcessState.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Models.DeviceCommands { /// /// Represents the state of a process running on an Android device. diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PackageInstallProgressState.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PackageInstallProgressState.cs index 9ac2ccf4..7834380d 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PackageInstallProgressState.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PackageInstallProgressState.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Models.DeviceCommands { /// /// Represents the state of the installation. diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PerProcessFlags.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PerProcessFlags.cs index 429321cc..474d2f08 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PerProcessFlags.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/Enums/PerProcessFlags.cs @@ -4,7 +4,7 @@ using System; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Models.DeviceCommands { /// /// Per process flags. diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/InstallProgressEventArgs.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/InstallProgressEventArgs.cs index c23d17aa..e572ea14 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Models/InstallProgressEventArgs.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/InstallProgressEventArgs.cs @@ -4,17 +4,17 @@ using System; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Models.DeviceCommands { /// /// Represents the state of the installation for . /// - public class InstallProgressEventArgs : EventArgs + public class InstallProgressEventArgs(PackageInstallProgressState state) : EventArgs { /// /// State of the installation. /// - public PackageInstallProgressState State { get; } + public PackageInstallProgressState State { get; } = state; /// /// Number of packages which is finished operation. @@ -22,7 +22,7 @@ public class InstallProgressEventArgs : EventArgs /// and /// state. /// - public int PackageFinished { get; } + public int PackageFinished { get; init; } /// /// Number of packages required for this operation. @@ -30,18 +30,13 @@ public class InstallProgressEventArgs : EventArgs /// and /// state. /// - public int PackageRequired { get; } + public int PackageRequired { get; init; } /// - /// Upload percentage completed. + /// Upload percentage (from to ) completed. /// Used only in state. /// - public double UploadProgress { get; } - - /// - /// Initializes a new instance of the class. - /// - public InstallProgressEventArgs(PackageInstallProgressState state) => State = state; + public double UploadProgress { get; init; } /// /// Initializes a new instance of the class. @@ -56,11 +51,13 @@ public InstallProgressEventArgs(int packageUploaded, int packageRequired, double /// /// Initializes a new instance of the class. - /// Which is used for and state. + /// Which is used for + /// and + /// state. /// - public InstallProgressEventArgs(int packageCleaned, int packageRequired, PackageInstallProgressState state) : this(state) + public InstallProgressEventArgs(int packageFinished, int packageRequired, PackageInstallProgressState state) : this(state) { - PackageFinished = packageCleaned; + PackageFinished = packageFinished; PackageRequired = packageRequired; } } diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/NamespaceDoc.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/NamespaceDoc.cs new file mode 100644 index 00000000..91d43186 --- /dev/null +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/NamespaceDoc.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace AdvancedSharpAdbClient.Models.DeviceCommands +{ + /// + /// The classes in this namespace provide models for . + /// + /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. + [CompilerGenerated] + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : Models.NamespaceDoc { } +} diff --git a/AdvancedSharpAdbClient/DeviceCommands/Models/VersionInfo.cs b/AdvancedSharpAdbClient/DeviceCommands/Models/VersionInfo.cs index c64b051a..a52af84b 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Models/VersionInfo.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Models/VersionInfo.cs @@ -2,46 +2,87 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient.DeviceCommands +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace AdvancedSharpAdbClient.Models.DeviceCommands { /// /// Represents a version of an Android application. /// - public class VersionInfo + /// The version code of the application. + /// The version name of the application. + public readonly record struct VersionInfo(int VersionCode, string VersionName) : IComparer, IComparer { /// /// Gets or sets the version code of an Android application. /// - public int VersionCode { get; } + public int VersionCode { get; init; } = VersionCode; /// /// Gets or sets the version name of an Android application. /// - public string VersionName { get; } + public string VersionName { get; init; } = VersionName; - /// - /// Initializes a new instance of the class. - /// - /// The version code of the application. - /// The version name of the application. - public VersionInfo(int versionCode, string versionName) - { - VersionCode = versionCode; - VersionName = versionName; - } + /// + public readonly int Compare([NotNull] object? x, [NotNull] object? y) => + x is VersionInfo left && y is VersionInfo right + ? left.VersionCode.CompareTo(right.VersionCode) + : throw new NotImplementedException(); + + /// + public readonly int Compare(VersionInfo x, VersionInfo y) => x.VersionCode.CompareTo(y.VersionCode); /// /// Deconstruct the class. /// /// The version code of the application. /// The version name of the application. - public void Deconstruct(out int versionCode, out string versionName) + public readonly void Deconstruct(out int versionCode, out string versionName) { versionCode = VersionCode; versionName = VersionName; } /// - public override string ToString() => $"{VersionName} ({VersionCode})"; + public override readonly string ToString() => $"{VersionName} ({VersionCode})"; + + /// + /// Compares the of two values to determine which is greater. + /// + /// The value to compare with . + /// The value to compare with . + /// if the of + /// is greater than ; otherwise, . + public static bool operator >(VersionInfo left, VersionInfo right) => left.VersionCode > right.VersionCode; + + /// + /// Compares the of two values to determine which is less. + /// + /// The value to compare with . + /// The value to compare with . + /// if the of + /// is less than ; otherwise, . + public static bool operator <(VersionInfo left, VersionInfo right) => left.VersionCode < right.VersionCode; + + /// + /// Compares the of two values to determine which is greater or equal. + /// + /// The value to compare with . + /// The value to compare with . + /// if the of + /// is greater or equal to ; otherwise, . + public static bool operator >=(VersionInfo left, VersionInfo right) => left.VersionCode >= right.VersionCode; + + /// + /// Compares the of two values to determine which is less or equal. + /// + /// The value to compare with . + /// The value to compare with . + /// if the of + /// is less or equal to ; otherwise, . + public static bool operator <=(VersionInfo left, VersionInfo right) => left.VersionCode <= right.VersionCode; } } diff --git a/AdvancedSharpAdbClient/DeviceCommands/NamespaceDoc.cs b/AdvancedSharpAdbClient/DeviceCommands/NamespaceDoc.cs index 4a19a9c9..e29ebbdb 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/NamespaceDoc.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/NamespaceDoc.cs @@ -2,6 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // +using System.ComponentModel; using System.Runtime.CompilerServices; namespace AdvancedSharpAdbClient.DeviceCommands @@ -11,5 +12,6 @@ namespace AdvancedSharpAdbClient.DeviceCommands /// /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. [CompilerGenerated] - internal class NamespaceDoc { } + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : AdvancedSharpAdbClient.NamespaceDoc { } } diff --git a/AdvancedSharpAdbClient/DeviceCommands/PackageManager.Async.cs b/AdvancedSharpAdbClient/DeviceCommands/PackageManager.Async.cs index f110464c..feb5aa9f 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/PackageManager.Async.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/PackageManager.Async.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using System.Threading; namespace AdvancedSharpAdbClient.DeviceCommands @@ -23,56 +23,92 @@ public virtual Task RefreshPackagesAsync(CancellationToken cancellationToken = d { ValidateDevice(); - PackageManagerReceiver pmr = new(Device, this); + StringBuilder requestBuilder = new StringBuilder().Append(ListFull); - return ThirdPartyOnly - ? client.ExecuteShellCommandAsync(Device, ListThirdPartyOnly, pmr, cancellationToken) - : client.ExecuteShellCommandAsync(Device, ListFull, pmr, cancellationToken); + if (Arguments != null) + { + foreach (string argument in Arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + string cmd = requestBuilder.ToString(); + PackageManagerReceiver pmr = new(this); + return AdbClient.ExecuteShellCommandAsync(Device, cmd, pmr, cancellationToken); } /// /// Installs an Android application on device. /// /// The absolute file system path to file on local host to install. - /// if re-install of app should be performed; otherwise, . + /// The arguments to pass to adb install. + /// A which represents the asynchronous operation. + public Task InstallPackageAsync(string packageFilePath, params string[] arguments) => + InstallPackageAsync(packageFilePath, default, arguments); + + /// + /// Installs an Android application on device. + /// + /// The absolute file system path to file on local host to install. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to adb install. /// A which represents the asynchronous operation. - public virtual async Task InstallPackageAsync(string packageFilePath, bool reinstall, CancellationToken cancellationToken = default) + public virtual async Task InstallPackageAsync(string packageFilePath, CancellationToken cancellationToken, params string[] arguments) { ValidateDevice(); - string remoteFilePath = await SyncPackageToDeviceAsync(packageFilePath, OnSyncProgressChanged, cancellationToken); - - await InstallRemotePackageAsync(remoteFilePath, reinstall, cancellationToken); + void OnSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) => + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is null ? 1 : 0, 1, args.ProgressPercentage)); + + string remoteFilePath = await SyncPackageToDeviceAsync(packageFilePath, OnSyncProgressChanged, cancellationToken).ConfigureAwait(false); + + await InstallRemotePackageAsync(remoteFilePath, cancellationToken, arguments).ConfigureAwait(false); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, 1, PackageInstallProgressState.PostInstall)); - await RemoveRemotePackageAsync(remoteFilePath, cancellationToken); + await RemoveRemotePackageAsync(remoteFilePath, cancellationToken).ConfigureAwait(false); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(1, 1, PackageInstallProgressState.PostInstall)); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Finished)); - - void OnSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) => - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is true ? 1 : 0, 1, args.ProgressPercentage)); } /// /// Installs the application package that was pushed to a temporary location on the device. /// /// absolute file path to package file on device. - /// Set to if re-install of app should be performed. + /// The arguments to pass to pm install. + /// A which represents the asynchronous operation. + public Task InstallRemotePackageAsync(string remoteFilePath, params string[] arguments) => + InstallRemotePackageAsync(remoteFilePath, default, arguments); + + /// + /// Installs the application package that was pushed to a temporary location on the device. + /// + /// absolute file path to package file on device. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm install. /// A which represents the asynchronous operation. - public virtual async Task InstallRemotePackageAsync(string remoteFilePath, bool reinstall, CancellationToken cancellationToken = default) + public virtual async Task InstallRemotePackageAsync(string remoteFilePath, CancellationToken cancellationToken, params string[] arguments) { InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Installing)); ValidateDevice(); - InstallOutputReceiver receiver = new(); - string reinstallSwitch = reinstall ? "-r " : string.Empty; + StringBuilder requestBuilder = new StringBuilder().Append("pm install"); + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } - string cmd = $"pm install {reinstallSwitch}\"{remoteFilePath}\""; - await client.ExecuteShellCommandAsync(Device, cmd, receiver, cancellationToken); + _ = requestBuilder.AppendFormat(" \"{0}\"", remoteFilePath); + + string cmd = requestBuilder.ToString(); + InstallOutputReceiver receiver = new(); + await AdbClient.ExecuteShellCommandAsync(Device, cmd, receiver, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { @@ -85,63 +121,80 @@ public virtual async Task InstallRemotePackageAsync(string remoteFilePath, bool /// /// The absolute base app file system path to file on local host to install. /// The absolute split app file system paths to file on local host to install. - /// Set to if re-install of app should be performed. + /// The arguments to pass to pm install-create. + /// A which represents the asynchronous operation. + public Task InstallMultiplePackageAsync(string basePackageFilePath, IEnumerable splitPackageFilePaths, params string[] arguments) => + InstallMultiplePackageAsync(basePackageFilePath, splitPackageFilePaths, default, arguments); + + /// + /// Installs Android multiple application on device. + /// + /// The absolute base app file system path to file on local host to install. + /// The absolute split app file system paths to file on local host to install. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm install-create. /// A which represents the asynchronous operation. - public virtual async Task InstallMultiplePackageAsync(string basePackageFilePath, IList splitPackageFilePaths, bool reinstall, CancellationToken cancellationToken = default) + public virtual async Task InstallMultiplePackageAsync(string basePackageFilePath, IEnumerable splitPackageFilePaths, CancellationToken cancellationToken, params string[] arguments) { ValidateDevice(); - void OnMainSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) => - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is true ? 1 : 0, splitPackageFilePaths.Count + 1, args.ProgressPercentage / 2)); + int splitPackageFileCount = splitPackageFilePaths.Count(); - string baseRemoteFilePath = await SyncPackageToDeviceAsync(basePackageFilePath, OnMainSyncProgressChanged, cancellationToken); + void OnMainSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) => + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is null ? 1 : 0, splitPackageFileCount + 1, args.ProgressPercentage / 2)); - Dictionary progress = new(splitPackageFilePaths.Count); - void OnSplitSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) - { - int count = 1; - if (sender is string path) - { - progress[path] = args.ProgressPercentage; - } - else if (sender is true) - { - count++; - } + string baseRemoteFilePath = await SyncPackageToDeviceAsync(basePackageFilePath, OnMainSyncProgressChanged, cancellationToken).ConfigureAwait(false); - double present = 0; - foreach (KeyValuePair info in progress) + int progressCount = 1; + Dictionary progress = new(splitPackageFileCount); + void OnSplitSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) + { + lock (progress) { - present += (info.Value / splitPackageFilePaths.Count) / 2; + if (sender is null) + { + progressCount++; + } + else if (sender is string path) + { + // Note: The progress may be less than the previous progress when async. + if (progress.TryGetValue(path, out double oldValue) + && oldValue > args.ProgressPercentage) + { + return; + } + progress[path] = args.ProgressPercentage; + } + double present = (progress.Values.Select(x => x / splitPackageFileCount).Sum() + 100) / 2; + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(progressCount, splitPackageFileCount + 1, present)); } - - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitPackageFilePaths.Count + 1, present)); } - List splitRemoteFilePaths = new(splitPackageFilePaths.Count); - IEnumerable tasks = splitPackageFilePaths.Select(async (x) => splitRemoteFilePaths.Add(await SyncPackageToDeviceAsync(x, OnSplitSyncProgressChanged, cancellationToken))); - foreach (Task task in tasks) + string[] splitRemoteFilePaths = await splitPackageFilePaths.Select(x => SyncPackageToDeviceAsync(x, OnSplitSyncProgressChanged, cancellationToken)).ToArrayAsync().ConfigureAwait(false); + + if (splitRemoteFilePaths.Length < splitPackageFileCount) { - await task; + throw new PackageInstallationException($"{nameof(SyncPackageToDeviceAsync)} failed. {splitPackageFileCount} should process but only {splitRemoteFilePaths.Length} processed."); } - await InstallMultipleRemotePackageAsync(baseRemoteFilePath, splitRemoteFilePaths, reinstall, cancellationToken); + await InstallMultipleRemotePackageAsync(baseRemoteFilePath, splitRemoteFilePaths, cancellationToken, arguments); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.PostInstall)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Length + 1, PackageInstallProgressState.PostInstall)); int count = 0; - tasks = splitRemoteFilePaths.Select(async (x) => + await Extensions.WhenAll(splitRemoteFilePaths.Select(async x => { - await RemoveRemotePackageAsync(x, cancellationToken); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(++count, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.PostInstall)); - }); - foreach (Task task in tasks) + count++; + await RemoveRemotePackageAsync(x, cancellationToken).ConfigureAwait(false); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitRemoteFilePaths.Length + 1, PackageInstallProgressState.PostInstall)); + })).ConfigureAwait(false); + + if (count < splitRemoteFilePaths.Length) { - await task; + throw new PackageInstallationException($"{nameof(RemoveRemotePackageAsync)} failed. {splitRemoteFilePaths.Length} should process but only {count} processed."); } - await RemoveRemotePackageAsync(baseRemoteFilePath, cancellationToken); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(++count, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.PostInstall)); + await RemoveRemotePackageAsync(baseRemoteFilePath, cancellationToken).ConfigureAwait(false); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(++count, splitRemoteFilePaths.Length + 1, PackageInstallProgressState.PostInstall)); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Finished)); } @@ -151,54 +204,71 @@ void OnSplitSyncProgressChanged(object sender, SyncProgressChangedEventArgs args /// /// The absolute split app file system paths to file on local host to install. /// The absolute package name of the base app. - /// Set to if re-install of app should be performed. + /// The arguments to pass to pm install-create. + /// A which represents the asynchronous operation. + public Task InstallMultiplePackageAsync(IEnumerable splitPackageFilePaths, string packageName, params string[] arguments) => + InstallMultiplePackageAsync(splitPackageFilePaths, packageName, default, arguments); + + /// + /// Installs Android multiple application on device. + /// + /// The absolute split app file system paths to file on local host to install. + /// The absolute package name of the base app. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm install-create. /// A which represents the asynchronous operation. - public virtual async Task InstallMultiplePackageAsync(IList splitPackageFilePaths, string packageName, bool reinstall, CancellationToken cancellationToken = default) + public virtual async Task InstallMultiplePackageAsync(IEnumerable splitPackageFilePaths, string packageName, CancellationToken cancellationToken, params string[] arguments) { ValidateDevice(); - Dictionary progress = new(splitPackageFilePaths.Count); - void OnSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) - { - int count = 1; - if (sender is string path) - { - progress[path] = args.ProgressPercentage; - } - else if (sender is true) - { - count++; - } + int splitPackageFileCount = splitPackageFilePaths.Count(); - double present = 0; - foreach (KeyValuePair info in progress) + int progressCount = 0; + Dictionary progress = new(splitPackageFileCount); + void OnSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) + { + lock (progress) { - present += (info.Value / splitPackageFilePaths.Count) / 2; + if (sender is null) + { + progressCount++; + } + else if (sender is string path) + { + // Note: The progress may be less than the previous progress when async. + if (progress.TryGetValue(path, out double oldValue) + && oldValue > args.ProgressPercentage) + { + return; + } + progress[path] = args.ProgressPercentage; + } + double present = progress.Values.Select(x => x / splitPackageFileCount).Sum(); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(progressCount, splitPackageFileCount, present)); } - - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitPackageFilePaths.Count, present)); } - List splitRemoteFilePaths = new(splitPackageFilePaths.Count); - IEnumerable tasks = splitPackageFilePaths.Select(async (x) => splitRemoteFilePaths.Add(await SyncPackageToDeviceAsync(x, OnSyncProgressChanged, cancellationToken))); - foreach (Task task in tasks) + string[] splitRemoteFilePaths = await splitPackageFilePaths.Select(x => SyncPackageToDeviceAsync(x, OnSyncProgressChanged, cancellationToken)).ToArrayAsync().ConfigureAwait(false); + + if (splitRemoteFilePaths.Length < splitPackageFileCount) { - await task; + throw new PackageInstallationException($"{nameof(SyncPackageToDeviceAsync)} failed. {splitPackageFileCount} should process but only {splitRemoteFilePaths.Length} processed."); } - await InstallMultipleRemotePackageAsync(splitRemoteFilePaths, packageName, reinstall, cancellationToken); + await InstallMultipleRemotePackageAsync(splitRemoteFilePaths, packageName, cancellationToken, arguments); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Count, PackageInstallProgressState.PostInstall)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Length, PackageInstallProgressState.PostInstall)); int count = 0; - tasks = splitRemoteFilePaths.Select(async (x) => + await Extensions.WhenAll(splitRemoteFilePaths.Select(async x => { - await RemoveRemotePackageAsync(x, cancellationToken); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(++count, splitRemoteFilePaths.Count, PackageInstallProgressState.PostInstall)); - }); - foreach (Task task in tasks) + count++; + await RemoveRemotePackageAsync(x, cancellationToken).ConfigureAwait(false); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitRemoteFilePaths.Length, PackageInstallProgressState.PostInstall)); + })).ConfigureAwait(false); + + if (count < splitRemoteFilePaths.Length) { - await task; + throw new PackageInstallationException($"{nameof(RemoveRemotePackageAsync)} failed. {splitRemoteFilePaths.Length} should process but only {count} processed."); } InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Finished)); @@ -209,45 +279,51 @@ void OnSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) /// /// The absolute base app file path to package file on device. /// The absolute split app file paths to package file on device. - /// Set to if re-install of app should be performed. + /// The arguments to pass to pm install-create. + /// A which represents the asynchronous operation. + public Task InstallMultipleRemotePackageAsync(string baseRemoteFilePath, IEnumerable splitRemoteFilePaths, params string[] arguments) => + InstallMultipleRemotePackageAsync(baseRemoteFilePath, splitRemoteFilePaths, default, arguments); + + /// + /// Installs the multiple application package that was pushed to a temporary location on the device. + /// + /// The absolute base app file path to package file on device. + /// The absolute split app file paths to package file on device. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm install-create. /// A which represents the asynchronous operation. - public virtual async Task InstallMultipleRemotePackageAsync(string baseRemoteFilePath, IList splitRemoteFilePaths, bool reinstall, CancellationToken cancellationToken = default) + public virtual async Task InstallMultipleRemotePackageAsync(string baseRemoteFilePath, IEnumerable splitRemoteFilePaths, CancellationToken cancellationToken, params string[] arguments) { InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.CreateSession)); ValidateDevice(); - string session = await CreateInstallSessionAsync(reinstall, cancellationToken: cancellationToken); + string session = await CreateInstallSessionAsync(cancellationToken: cancellationToken, arguments: arguments).ConfigureAwait(false); + + int splitRemoteFileCount = splitRemoteFilePaths.Count(); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.WriteSession)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFileCount + 1, PackageInstallProgressState.WriteSession)); - await WriteInstallSessionAsync(session, "base", baseRemoteFilePath, cancellationToken); + await WriteInstallSessionAsync(session, "base", baseRemoteFilePath, cancellationToken).ConfigureAwait(false); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(1, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.WriteSession)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(1, splitRemoteFileCount + 1, PackageInstallProgressState.WriteSession)); - int i = 0, count = 0; - IEnumerable tasks = splitRemoteFilePaths.Select(async (splitRemoteFilePath) => + int count = 0; + await Extensions.WhenAll(splitRemoteFilePaths.Select(async (splitRemoteFilePath) => { - try - { - await WriteInstallSessionAsync(session, $"splitapp{i++}", splitRemoteFilePath, cancellationToken); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(++count, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.WriteSession)); - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } - }); - foreach (Task task in tasks) + await WriteInstallSessionAsync(session, $"split{count++}", splitRemoteFilePath, cancellationToken).ConfigureAwait(false); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitRemoteFileCount + 1, PackageInstallProgressState.WriteSession)); + })).ConfigureAwait(false); + + if (count < splitRemoteFileCount) { - await task; + throw new PackageInstallationException($"{nameof(WriteInstallSessionAsync)} failed. {splitRemoteFileCount} should process but only {count} processed."); } InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Installing)); InstallOutputReceiver receiver = new(); - await client.ExecuteShellCommandAsync(Device, $"pm install-commit {session}", receiver, cancellationToken); + await AdbClient.ExecuteShellCommandAsync(Device, $"pm install-commit {session}", receiver, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { @@ -260,41 +336,47 @@ public virtual async Task InstallMultipleRemotePackageAsync(string baseRemoteFil /// /// The absolute split app file paths to package file on device. /// The absolute package name of the base app. - /// Set to if re-install of app should be performed. + /// The arguments to pass to pm install-create. + /// A which represents the asynchronous operation. + public Task InstallMultipleRemotePackageAsync(IEnumerable splitRemoteFilePaths, string packageName, params string[] arguments) => + InstallMultipleRemotePackageAsync(splitRemoteFilePaths, packageName, default, arguments); + + /// + /// Installs the multiple application package that was pushed to a temporary location on the device. + /// + /// The absolute split app file paths to package file on device. + /// The absolute package name of the base app. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm install-create. /// A which represents the asynchronous operation. - public virtual async Task InstallMultipleRemotePackageAsync(IList splitRemoteFilePaths, string packageName, bool reinstall, CancellationToken cancellationToken = default) + public virtual async Task InstallMultipleRemotePackageAsync(IEnumerable splitRemoteFilePaths, string packageName, CancellationToken cancellationToken, params string[] arguments) { InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.CreateSession)); ValidateDevice(); - string session = await CreateInstallSessionAsync(reinstall, packageName, cancellationToken); + string session = await CreateInstallSessionAsync(packageName, cancellationToken, arguments).ConfigureAwait(false); + + int splitRemoteFileCount = splitRemoteFilePaths.Count(); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Count, PackageInstallProgressState.WriteSession)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFileCount, PackageInstallProgressState.WriteSession)); - int i = 0, count = 0; - IEnumerable tasks = splitRemoteFilePaths.Select(async (splitRemoteFilePath) => + int count = 0; + await Extensions.WhenAll(splitRemoteFilePaths.Select(async (splitRemoteFilePath) => { - try - { - await WriteInstallSessionAsync(session, $"splitapp{i++}", splitRemoteFilePath, cancellationToken); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(++count, splitRemoteFilePaths.Count, PackageInstallProgressState.WriteSession)); - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } - }); - foreach (Task task in tasks) + await WriteInstallSessionAsync(session, $"split{count++}", splitRemoteFilePath, cancellationToken).ConfigureAwait(false); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitRemoteFileCount, PackageInstallProgressState.WriteSession)); + })).ConfigureAwait(false); + + if (count < splitRemoteFileCount) { - await task; + throw new PackageInstallationException($"{nameof(WriteInstallSessionAsync)} failed. {splitRemoteFileCount} should process but only {count} processed."); } InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Installing)); InstallOutputReceiver receiver = new(); - await client.ExecuteShellCommandAsync(Device, $"pm install-commit {session}", receiver, cancellationToken); + await AdbClient.ExecuteShellCommandAsync(Device, $"pm install-commit {session}", receiver, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { @@ -302,18 +384,41 @@ public virtual async Task InstallMultipleRemotePackageAsync(IList splitR } } + /// + /// Uninstalls a package from the device. + /// + /// The name of the package to uninstall. + /// The arguments to pass to pm uninstall. + /// A which represents the asynchronous operation. + public Task UninstallPackageAsync(string packageName, params string[] arguments) => + UninstallPackageAsync(packageName, default, arguments); + /// /// Uninstalls a package from the device. /// /// The name of the package to uninstall. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm uninstall. /// A which represents the asynchronous operation. - public virtual async Task UninstallPackageAsync(string packageName, CancellationToken cancellationToken = default) + public virtual async Task UninstallPackageAsync(string packageName, CancellationToken cancellationToken, params string[] arguments) { ValidateDevice(); + StringBuilder requestBuilder = new StringBuilder().Append("pm uninstall"); + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + _ = requestBuilder.AppendFormat(" {0}", packageName); + + string cmd = requestBuilder.ToString(); InstallOutputReceiver receiver = new(); - await client.ExecuteShellCommandAsync(Device, $"pm uninstall {packageName}", receiver, cancellationToken); + await AdbClient.ExecuteShellCommandAsync(Device, cmd, receiver, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { throw new PackageInstallationException(receiver.ErrorMessage); @@ -331,7 +436,7 @@ public virtual async Task GetVersionInfoAsync(string packageName, C ValidateDevice(); VersionInfoReceiver receiver = new(); - await client.ExecuteShellCommandAsync(Device, $"dumpsys package {packageName}", receiver, cancellationToken); + await AdbClient.ExecuteShellCommandAsync(Device, $"dumpsys package {packageName}", receiver, cancellationToken).ConfigureAwait(false); return receiver.VersionInfo; } @@ -343,9 +448,9 @@ public virtual async Task GetVersionInfoAsync(string packageName, C /// A which can be used to cancel the asynchronous operation. /// A which return the destination path on device for file. /// If fatal error occurred when pushing file. - protected virtual async Task SyncPackageToDeviceAsync(string localFilePath, Action progress, CancellationToken cancellationToken = default) + protected virtual async Task SyncPackageToDeviceAsync(string localFilePath, Action? progress, CancellationToken cancellationToken = default) { - progress(localFilePath, new SyncProgressChangedEventArgs(0, 0)); + progress?.Invoke(localFilePath, new SyncProgressChangedEventArgs(0, 100)); ValidateDevice(); @@ -358,44 +463,41 @@ protected virtual async Task SyncPackageToDeviceAsync(string localFilePa // workitem: 19711 string remoteFilePath = LinuxPath.Combine(TempInstallationDirectory, packageFileName); -#if HAS_LOGGER - logger.LogDebug(packageFileName, $"Uploading {packageFileName} onto device '{Device.Serial}'"); -#endif + logger.LogDebug("Uploading {0} onto device '{1}'", packageFileName, Device.Serial); - using (ISyncService sync = syncServiceFactory(client, Device)) + using (ISyncService sync = syncServiceFactory(AdbClient, Device)) { + void OnSyncProgressChanged(object? sender, SyncProgressChangedEventArgs args) => progress!(localFilePath, args); + if (progress != null) { - sync.SyncProgressChanged += (sender, e) => progress(localFilePath, e); + sync.SyncProgressChanged -= OnSyncProgressChanged; + sync.SyncProgressChanged += OnSyncProgressChanged; } #if NETCOREAPP3_0_OR_GREATER await #endif - using Stream stream = File.OpenRead(localFilePath); -#if HAS_LOGGER - logger.LogDebug($"Uploading file onto device '{Device.Serial}'"); -#endif + using FileStream stream = File.OpenRead(localFilePath); + + logger.LogDebug("Uploading file onto device '{0}'", Device.Serial); // As C# can't use octal, the octal literal 666 (rw-Permission) is here converted to decimal (438) - await sync.PushAsync(stream, remoteFilePath, 438, File.GetLastWriteTime(localFilePath), null, cancellationToken); + await sync.PushAsync(stream, remoteFilePath, 438, File.GetLastWriteTime(localFilePath), null, cancellationToken).ConfigureAwait(false); + + sync.SyncProgressChanged -= OnSyncProgressChanged; } return remoteFilePath; } -#if HAS_LOGGER catch (IOException e) { - logger.LogError(e, $"Unable to open sync connection! reason: {e.Message}"); -#else - catch (IOException) - { -#endif + logger.LogError(e, "Unable to open sync connection! reason: {0}", e.Message); throw; } finally { - progress(true, new SyncProgressChangedEventArgs(0, 0)); + progress?.Invoke(null, new SyncProgressChangedEventArgs(100, 100)); } } @@ -411,16 +513,11 @@ protected virtual async Task RemoveRemotePackageAsync(string remoteFilePath, Can // now we delete the app we synced try { - await client.ExecuteShellCommandAsync(Device, $"rm \"{remoteFilePath}\"", null, cancellationToken); + await AdbClient.ExecuteShellCommandAsync(Device, $"rm \"{remoteFilePath}\"", cancellationToken).ConfigureAwait(false); } -#if HAS_LOGGER catch (IOException e) { - logger.LogError(e, $"Failed to delete temporary package: {e.Message}"); -#else - catch (IOException) - { -#endif + logger.LogError(e, "Failed to delete temporary package: {0}", e.Message); throw; } } @@ -428,29 +525,41 @@ protected virtual async Task RemoveRemotePackageAsync(string remoteFilePath, Can /// /// Like "install", but starts an install session. /// - /// Set to if re-install of app should be performed. /// The absolute package name of the base app. /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to pm install-create. /// A which return the session ID. - protected virtual async Task CreateInstallSessionAsync(bool reinstall, string packageName = null, CancellationToken cancellationToken = default) + protected virtual async Task CreateInstallSessionAsync(string? packageName = null, CancellationToken cancellationToken = default, params string[] arguments) { ValidateDevice(); - InstallOutputReceiver receiver = new(); - string reinstallSwitch = reinstall ? " -r" : string.Empty; - string addon = packageName.IsNullOrWhiteSpace() ? string.Empty : $" -p {packageName}"; + StringBuilder requestBuilder = new StringBuilder().Append("pm install-create"); + + if (!StringExtensions.IsNullOrWhiteSpace(packageName)) + { + _ = requestBuilder.AppendFormat(" -p {0}", packageName); + } + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } - string cmd = $"pm install-create{reinstallSwitch}{addon}"; - await client.ExecuteShellCommandAsync(Device, cmd, receiver, cancellationToken); + string cmd = requestBuilder.ToString(); + InstallOutputReceiver receiver = new(); + await AdbClient.ExecuteShellCommandAsync(Device, cmd, receiver, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(receiver.SuccessMessage)) { throw new PackageInstallationException(receiver.ErrorMessage); } - string result = receiver.SuccessMessage; - int arr = result.IndexOf("]") - 1 - result.IndexOf("["); - string session = result.Substring(result.IndexOf("[") + 1, arr); + string result = receiver.SuccessMessage ?? throw new AdbException($"The {nameof(result)} of {nameof(CreateInstallSessionAsync)} is null."); + int arr = result.IndexOf(']') - 1 - result.IndexOf('['); + string session = result.Substring(result.IndexOf('[') + 1, arr); return session; } @@ -468,7 +577,7 @@ protected virtual async Task WriteInstallSessionAsync(string session, string apk ValidateDevice(); InstallOutputReceiver receiver = new(); - await client.ExecuteShellCommandAsync(Device, $"pm install-write {session} {apkName}.apk \"{path}\"", receiver, cancellationToken); + await AdbClient.ExecuteShellCommandAsync(Device, $"pm install-write {session} {apkName}.apk \"{path}\"", receiver, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { diff --git a/AdvancedSharpAdbClient/DeviceCommands/PackageManager.cs b/AdvancedSharpAdbClient/DeviceCommands/PackageManager.cs index 9f56ba00..72331e96 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/PackageManager.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/PackageManager.cs @@ -2,12 +2,11 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Threading; +using System.Linq; +using System.Text; namespace AdvancedSharpAdbClient.DeviceCommands { @@ -26,22 +25,10 @@ public partial class PackageManager /// protected const string ListFull = "pm list packages -f"; - /// - /// The command that list all third party packages installed on the device. - /// - protected const string ListThirdPartyOnly = "pm list packages -f -3"; - -#if HAS_LOGGER /// /// The logger to use when logging messages. /// protected readonly ILogger logger; -#endif - - /// - /// The to use when communicating with the device. - /// - protected readonly IAdbClient client; /// /// A function which returns a new instance of a class @@ -53,34 +40,26 @@ public partial class PackageManager /// /// Occurs when there is a change in the status of the installing. /// - public event EventHandler InstallProgressChanged; + public event EventHandler? InstallProgressChanged; -#if !HAS_LOGGER -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// /// Initializes a new instance of the class. /// /// The to use to communicate with the Android Debug Bridge. /// The device on which to look for packages. - /// to only indicate third party applications; - /// to also include built-in applications. /// A function which returns a new instance of a class /// that implements the interface, /// that can be used to transfer files to and from a given device. /// A value indicating whether to skip the initial refresh of the package list or not. /// Used mainly by unit tests. /// The logger to use when logging. - public PackageManager(IAdbClient client, DeviceData device, bool thirdPartyOnly = false, Func syncServiceFactory = null, bool skipInit = false -#if HAS_LOGGER - , ILogger logger = null -#endif - ) + /// The arguments to pass to pm list packages. + public PackageManager(IAdbClient client, DeviceData device, Func? syncServiceFactory = null, bool skipInit = false, ILogger? logger = null, params string[] arguments) { Device = device ?? throw new ArgumentNullException(nameof(device)); - Packages = new Dictionary(); - ThirdPartyOnly = thirdPartyOnly; - this.client = client ?? throw new ArgumentNullException(nameof(client)); + Packages = []; + AdbClient = client ?? throw new ArgumentNullException(nameof(client)); + Arguments = arguments; this.syncServiceFactory = syncServiceFactory ?? Factories.SyncServiceFactory; @@ -89,19 +68,38 @@ public PackageManager(IAdbClient client, DeviceData device, bool thirdPartyOnly RefreshPackages(); } -#if HAS_LOGGER - this.logger = logger ?? NullLogger.Instance; -#endif + this.logger = logger ?? LoggerProvider.CreateLogger(); } -#if !HAS_LOGGER -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// - /// Gets a value indicating whether this package manager only lists third party applications, - /// or also includes built-in applications. + /// Gets or sets a value to pass to pm list packages when list packages. /// - public bool ThirdPartyOnly { get; private set; } + /// + /// + /// + /// -a: all known packages (but excluding APEXes) + /// + /// + /// -d: filter to only show disabled packages + /// + /// + /// -e: filter to only show enabled packages + /// + /// + /// -s: filter to only show system packages + /// + /// + /// -3: filter to only show third party packages + /// + /// + /// -i: ignored (used for compatibility with older releases) + /// + /// + /// -u: also include uninstalled packages + /// + /// + /// + public string[] Arguments { get; set; } /// /// Gets the list of packages currently installed on the device. They key is the name of the package; @@ -112,7 +110,12 @@ public PackageManager(IAdbClient client, DeviceData device, bool thirdPartyOnly /// /// Gets the device. /// - public DeviceData Device { get; private set; } + public DeviceData Device { get; init; } + + /// + /// The to use when communicating with the device. + /// + protected IAdbClient AdbClient { get; init; } /// /// Refreshes the packages. @@ -121,57 +124,70 @@ public virtual void RefreshPackages() { ValidateDevice(); - PackageManagerReceiver pmr = new(Device, this); + StringBuilder requestBuilder = new StringBuilder().Append(ListFull); - if (ThirdPartyOnly) + if (Arguments != null) { - client.ExecuteShellCommand(Device, ListThirdPartyOnly, pmr); - } - else - { - client.ExecuteShellCommand(Device, ListFull, pmr); + foreach (string argument in Arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } } + + string cmd = requestBuilder.ToString(); + PackageManagerReceiver pmr = new(this); + AdbClient.ExecuteShellCommand(Device, cmd, pmr); } /// /// Installs an Android application on device. /// /// The absolute file system path to file on local host to install. - /// if re-install of app should be performed; otherwise, . - public virtual void InstallPackage(string packageFilePath, bool reinstall) + /// The arguments to pass to adb install. + public virtual void InstallPackage(string packageFilePath, params string[] arguments) { ValidateDevice(); + void OnSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) => + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is null ? 1 : 0, 1, args.ProgressPercentage)); + string remoteFilePath = SyncPackageToDevice(packageFilePath, OnSyncProgressChanged); - InstallRemotePackage(remoteFilePath, reinstall); + InstallRemotePackage(remoteFilePath, arguments); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, 1, PackageInstallProgressState.PostInstall)); RemoveRemotePackage(remoteFilePath); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(1, 1, PackageInstallProgressState.PostInstall)); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Finished)); - - void OnSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) => - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is true ? 1 : 0, 1, args.ProgressPercentage)); } /// /// Installs the application package that was pushed to a temporary location on the device. /// /// absolute file path to package file on device. - /// Set to if re-install of app should be performed. - public virtual void InstallRemotePackage(string remoteFilePath, bool reinstall) + /// The arguments to pass to adb install. + public virtual void InstallRemotePackage(string remoteFilePath, params string[] arguments) { InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Installing)); ValidateDevice(); - InstallOutputReceiver receiver = new(); - string reinstallSwitch = reinstall ? "-r " : string.Empty; + StringBuilder requestBuilder = new StringBuilder().Append("pm install"); + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + _ = requestBuilder.AppendFormat(" \"{0}\"", remoteFilePath); - string cmd = $"pm install {reinstallSwitch}\"{remoteFilePath}\""; - client.ExecuteShellCommand(Device, cmd, receiver); + string cmd = requestBuilder.ToString(); + InstallOutputReceiver receiver = new(); + AdbClient.ExecuteShellCommand(Device, cmd, receiver); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { @@ -184,48 +200,48 @@ public virtual void InstallRemotePackage(string remoteFilePath, bool reinstall) /// /// The absolute base app file system path to file on local host to install. /// The absolute split app file system paths to file on local host to install. - /// Set to if re-install of app should be performed. - public virtual void InstallMultiplePackage(string basePackageFilePath, IList splitPackageFilePaths, bool reinstall) + /// The arguments to pass to pm install-create. + public virtual void InstallMultiplePackage(string basePackageFilePath, IEnumerable splitPackageFilePaths, params string[] arguments) { ValidateDevice(); - void OnMainSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) => - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is true ? 1 : 0, splitPackageFilePaths.Count + 1, args.ProgressPercentage / 2)); + int splitPackageFileCount = splitPackageFilePaths.Count(); + + void OnMainSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) => + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(sender is null ? 1 : 0, splitPackageFileCount + 1, args.ProgressPercentage / 2)); string baseRemoteFilePath = SyncPackageToDevice(basePackageFilePath, OnMainSyncProgressChanged); - Dictionary progress = new(splitPackageFilePaths.Count); - void OnSplitSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) + int progressCount = 1; + Dictionary progress = new(splitPackageFileCount); + void OnSplitSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) { - int count = 1; - if (sender is string path) + lock (progress) { - progress[path] = args.ProgressPercentage; - } - else if (sender is true) - { - count++; - } - - double present = 0; - foreach(KeyValuePair info in progress) - { - present += (info.Value / splitPackageFilePaths.Count) / 2; + if (sender is null) + { + progressCount++; + } + else if (sender is string path) + { + progress[path] = args.ProgressPercentage; + } + double present = (progress.Values.Select(x => x / splitPackageFileCount).Sum() + 100) / 2; + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(progressCount, splitPackageFileCount + 1, present)); } - - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitPackageFilePaths.Count + 1, present)); } - string[] splitRemoteFilePaths = new string[splitPackageFilePaths.Count]; - for (int i = 0; i < splitPackageFilePaths.Count; i++) + int i = 0; + string[] splitRemoteFilePaths = new string[splitPackageFileCount]; + foreach (string splitPackageFilePath in splitPackageFilePaths) { - splitRemoteFilePaths[i] = SyncPackageToDevice(splitPackageFilePaths[i], OnSplitSyncProgressChanged); + splitRemoteFilePaths[i++] = SyncPackageToDevice(splitPackageFilePath, OnSplitSyncProgressChanged); } - InstallMultipleRemotePackage(baseRemoteFilePath, splitRemoteFilePaths, reinstall); + InstallMultipleRemotePackage(baseRemoteFilePath, splitRemoteFilePaths, arguments); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Length + 1, PackageInstallProgressState.PostInstall)); int count = 0; + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Length + 1, PackageInstallProgressState.PostInstall)); foreach (string splitRemoteFilePath in splitRemoteFilePaths) { RemoveRemotePackage(splitRemoteFilePath); @@ -243,40 +259,40 @@ void OnSplitSyncProgressChanged(object sender, SyncProgressChangedEventArgs args /// /// The absolute split app file system paths to file on local host to install. /// The absolute package name of the base app. - /// Set to if re-install of app should be performed. - public virtual void InstallMultiplePackage(IList splitPackageFilePaths, string packageName, bool reinstall) + /// The arguments to pass to pm install-create. + public virtual void InstallMultiplePackage(IEnumerable splitPackageFilePaths, string packageName, params string[] arguments) { ValidateDevice(); - Dictionary progress = new(splitPackageFilePaths.Count); - void OnSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) - { - int count = 1; - if (sender is string path) - { - progress[path] = args.ProgressPercentage; - } - else if (sender is true) - { - count++; - } + int splitPackageFileCount = splitPackageFilePaths.Count(); - double present = 0; - foreach (KeyValuePair info in progress) + int progressCount = 0; + Dictionary progress = new(splitPackageFileCount); + void OnSyncProgressChanged(string? sender, SyncProgressChangedEventArgs args) + { + lock (progress) { - present += info.Value / splitPackageFilePaths.Count; + if (sender is null) + { + progressCount++; + } + else if (sender is string path) + { + progress[path] = args.ProgressPercentage; + } + double present = progress.Values.Select(x => x / splitPackageFileCount).Sum(); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(progressCount, splitPackageFileCount, present)); } - - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitPackageFilePaths.Count, present)); } - string[] splitRemoteFilePaths = new string[splitPackageFilePaths.Count]; - for (int i = 0; i < splitPackageFilePaths.Count; i++) + int i = 0; + string[] splitRemoteFilePaths = new string[splitPackageFileCount]; + foreach (string splitPackageFilePath in splitPackageFilePaths) { - splitRemoteFilePaths[i] = SyncPackageToDevice(splitPackageFilePaths[i], OnSyncProgressChanged); + splitRemoteFilePaths[i++] = SyncPackageToDevice(splitPackageFilePath, OnSyncProgressChanged); } - InstallMultipleRemotePackage(splitRemoteFilePaths, packageName, reinstall); + InstallMultipleRemotePackage(splitRemoteFilePaths, packageName, arguments); InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Length, PackageInstallProgressState.PostInstall)); int count = 0; @@ -294,39 +310,34 @@ void OnSyncProgressChanged(object sender, SyncProgressChangedEventArgs args) /// /// The absolute base app file path to package file on device. /// The absolute split app file paths to package file on device. - /// Set to if re-install of app should be performed. - public virtual void InstallMultipleRemotePackage(string baseRemoteFilePath, IList splitRemoteFilePaths, bool reinstall) + /// The arguments to pass to pm install-create. + public virtual void InstallMultipleRemotePackage(string baseRemoteFilePath, IEnumerable splitRemoteFilePaths, params string[] arguments) { InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.CreateSession)); ValidateDevice(); - string session = CreateInstallSession(reinstall); + string session = CreateInstallSession(arguments: arguments); + + int splitRemoteFileCount = splitRemoteFilePaths.Count(); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.WriteSession)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFileCount + 1, PackageInstallProgressState.WriteSession)); WriteInstallSession(session, "base", baseRemoteFilePath); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(1, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.WriteSession)); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(1, splitRemoteFileCount + 1, PackageInstallProgressState.WriteSession)); - int i = 0; + int count = 0; foreach (string splitRemoteFilePath in splitRemoteFilePaths) { - try - { - WriteInstallSession(session, $"splitapp{i++}", splitRemoteFilePath); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(i, splitRemoteFilePaths.Count + 1, PackageInstallProgressState.WriteSession)); - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } + WriteInstallSession(session, $"split{count++}", splitRemoteFilePath); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitRemoteFileCount + 1, PackageInstallProgressState.WriteSession)); } InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Installing)); InstallOutputReceiver receiver = new(); - client.ExecuteShellCommand(Device, $"pm install-commit {session}", receiver); + AdbClient.ExecuteShellCommand(Device, $"pm install-commit {session}", receiver); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { @@ -339,35 +350,30 @@ public virtual void InstallMultipleRemotePackage(string baseRemoteFilePath, ILis /// /// The absolute split app file paths to package file on device. /// The absolute package name of the base app. - /// Set to if re-install of app should be performed. - public virtual void InstallMultipleRemotePackage(IList splitRemoteFilePaths, string packageName, bool reinstall) + /// The arguments to pass to pm install-create. + public virtual void InstallMultipleRemotePackage(IEnumerable splitRemoteFilePaths, string packageName, params string[] arguments) { InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.CreateSession)); ValidateDevice(); - string session = CreateInstallSession(reinstall, packageName); + string session = CreateInstallSession(packageName, arguments); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFilePaths.Count, PackageInstallProgressState.WriteSession)); + int splitRemoteFileCount = splitRemoteFilePaths.Count(); - int i = 0; + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(0, splitRemoteFileCount, PackageInstallProgressState.WriteSession)); + + int count = 0; foreach (string splitRemoteFilePath in splitRemoteFilePaths) { - try - { - WriteInstallSession(session, $"splitapp{i++}", splitRemoteFilePath); - InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(i, splitRemoteFilePaths.Count, PackageInstallProgressState.WriteSession)); - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } + WriteInstallSession(session, $"split{count++}", splitRemoteFilePath); + InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(count, splitRemoteFileCount, PackageInstallProgressState.WriteSession)); } InstallProgressChanged?.Invoke(this, new InstallProgressEventArgs(PackageInstallProgressState.Installing)); InstallOutputReceiver receiver = new(); - client.ExecuteShellCommand(Device, $"pm install-commit {session}", receiver); + AdbClient.ExecuteShellCommand(Device, $"pm install-commit {session}", receiver); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { @@ -379,12 +385,26 @@ public virtual void InstallMultipleRemotePackage(IList splitRemoteFilePa /// Uninstalls a package from the device. /// /// The name of the package to uninstall. - public virtual void UninstallPackage(string packageName) + /// The arguments to pass to pm uninstall. + public virtual void UninstallPackage(string packageName, params string[] arguments) { ValidateDevice(); + StringBuilder requestBuilder = new StringBuilder().Append("pm uninstall"); + + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + _ = requestBuilder.AppendFormat(" {0}", packageName); + + string cmd = requestBuilder.ToString(); InstallOutputReceiver receiver = new(); - client.ExecuteShellCommand(Device, $"pm uninstall {packageName}", receiver); + AdbClient.ExecuteShellCommand(Device, cmd, receiver); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { throw new PackageInstallationException(receiver.ErrorMessage); @@ -401,7 +421,7 @@ public virtual VersionInfo GetVersionInfo(string packageName) ValidateDevice(); VersionInfoReceiver receiver = new(); - client.ExecuteShellCommand(Device, $"dumpsys package {packageName}", receiver); + AdbClient.ExecuteShellCommand(Device, $"dumpsys package {packageName}", receiver); return receiver.VersionInfo; } @@ -423,9 +443,9 @@ protected void ValidateDevice() /// An optional parameter which, when specified, returns progress notifications. /// Destination path on device for file. /// If fatal error occurred when pushing file. - protected virtual string SyncPackageToDevice(string localFilePath, Action progress) + protected virtual string SyncPackageToDevice(string localFilePath, Action? progress) { - progress(localFilePath, new SyncProgressChangedEventArgs(0, 0)); + progress?.Invoke(localFilePath, new SyncProgressChangedEventArgs(0, 100)); ValidateDevice(); @@ -438,45 +458,38 @@ protected virtual string SyncPackageToDevice(string localFilePath, Action progress!(localFilePath, args); + if (progress != null) { - sync.SyncProgressChanged += (sender, e) => progress(localFilePath, e); + sync.SyncProgressChanged -= OnSyncProgressChanged; + sync.SyncProgressChanged += OnSyncProgressChanged; } - using Stream stream = File.OpenRead(localFilePath); -#if HAS_LOGGER - logger.LogDebug($"Uploading file onto device '{Device.Serial}'"); -#endif + using FileStream stream = File.OpenRead(localFilePath); + + logger.LogDebug("Uploading file onto device '{0}'", Device.Serial); // As C# can't use octal, the octal literal 666 (rw-Permission) is here converted to decimal (438) - sync.Push(stream, remoteFilePath, 438, File.GetLastWriteTime(localFilePath), null -#if HAS_TASK - , CancellationToken.None -#endif - ); + sync.Push(stream, remoteFilePath, 438, File.GetLastWriteTime(localFilePath), null, false); + + sync.SyncProgressChanged -= OnSyncProgressChanged; } return remoteFilePath; } -#if HAS_LOGGER catch (IOException e) { - logger.LogError(e, $"Unable to open sync connection! reason: {e.Message}"); -#else - catch (IOException) - { -#endif + logger.LogError(e, "Unable to open sync connection! reason: {0}", e.Message); throw; } finally { - progress(true, new SyncProgressChangedEventArgs(0, 0)); + progress?.Invoke(null, new SyncProgressChangedEventArgs(100, 100)); } } @@ -490,16 +503,11 @@ protected virtual void RemoveRemotePackage(string remoteFilePath) // now we delete the app we synced try { - client.ExecuteShellCommand(Device, $"rm \"{remoteFilePath}\"", null); + AdbClient.ExecuteShellCommand(Device, $"rm \"{remoteFilePath}\""); } -#if HAS_LOGGER catch (IOException e) { - logger.LogError(e, $"Failed to delete temporary package: {e.Message}"); -#else - catch (IOException) - { -#endif + logger.LogError(e, "Failed to delete temporary package: {0}", e.Message); throw; } } @@ -507,28 +515,40 @@ protected virtual void RemoveRemotePackage(string remoteFilePath) /// /// Like "install", but starts an install session. /// - /// Set to if re-install of app should be performed. /// The absolute package name of the base app. + /// The arguments to pass to pm install-create. /// Session ID. - protected virtual string CreateInstallSession(bool reinstall, string packageName = null) + protected virtual string CreateInstallSession(string? packageName = null, params string[] arguments) { ValidateDevice(); - InstallOutputReceiver receiver = new(); - string reinstallSwitch = reinstall ? " -r" : string.Empty; - string addon = packageName.IsNullOrWhiteSpace() ? string.Empty : $" -p {packageName}"; + StringBuilder requestBuilder = new StringBuilder().Append("pm install-create"); + + if (!StringExtensions.IsNullOrWhiteSpace(packageName)) + { + _ = requestBuilder.AppendFormat(" -p {0}", packageName); + } - string cmd = $"pm install-create{reinstallSwitch}{addon}"; - client.ExecuteShellCommand(Device, cmd, receiver); + if (arguments != null) + { + foreach (string argument in arguments) + { + _ = requestBuilder.AppendFormat(" {0}", argument); + } + } + + string cmd = requestBuilder.ToString(); + InstallOutputReceiver receiver = new(); + AdbClient.ExecuteShellCommand(Device, cmd, receiver); if (string.IsNullOrEmpty(receiver.SuccessMessage)) { throw new PackageInstallationException(receiver.ErrorMessage); } - string result = receiver.SuccessMessage; - int arr = result.IndexOf("]") - 1 - result.IndexOf("["); - string session = result.Substring(result.IndexOf("[") + 1, arr); + string result = receiver.SuccessMessage ?? throw new AdbException($"The {nameof(result)} of {nameof(CreateInstallSession)} is null."); + int arr = result.IndexOf(']') - 1 - result.IndexOf('['); + string session = result.Substring(result.IndexOf('[') + 1, arr); return session; } @@ -544,7 +564,7 @@ protected virtual void WriteInstallSession(string session, string apkName, strin ValidateDevice(); InstallOutputReceiver receiver = new(); - client.ExecuteShellCommand(Device, $"pm install-write {session} {apkName}.apk \"{path}\"", receiver); + AdbClient.ExecuteShellCommand(Device, $"pm install-write {session} {apkName}.apk \"{path}\"", receiver); if (!string.IsNullOrEmpty(receiver.ErrorMessage)) { diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/EnvironmentVariablesReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/EnvironmentVariablesReceiver.cs index 4df27c4d..40d72da0 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/EnvironmentVariablesReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/EnvironmentVariablesReceiver.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Processes the output of the printenv command, which dumps all environment variables of an Android device. @@ -25,12 +25,12 @@ public sealed partial class EnvironmentVariablesReceiver : MultiLineReceiver /// /// Initializes a new instance of the class. /// - public EnvironmentVariablesReceiver() => EnvironmentVariables = new Dictionary(); + public EnvironmentVariablesReceiver() { } /// /// Gets the environment variables that are currently defined on the device. /// - public Dictionary EnvironmentVariables { get; private set; } + public Dictionary EnvironmentVariables { get; } = []; /// /// Processes the new lines. @@ -41,7 +41,7 @@ protected override void ProcessNewLines(IEnumerable lines) Regex regex = EnvRegex(); foreach (string line in lines) { - if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + if (string.IsNullOrEmpty(line) || line.StartsWith('#')) { continue; } diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/GetPropReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/GetPropReceiver.cs index 89435e1b..a7caf189 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/GetPropReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/GetPropReceiver.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Parses the output of the getprop command, which lists all properties of an Android device. @@ -22,10 +22,15 @@ public sealed partial class GetPropReceiver : MultiLineReceiver /// private const string GetPropPattern = "^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"; + /// + /// Initializes a new instance of the class. + /// + public GetPropReceiver() { } + /// /// Gets the list of properties which have been retrieved. /// - public Dictionary Properties { get; private set; } = new Dictionary(); + public Dictionary Properties { get; } = []; /// /// Processes the new lines. @@ -41,7 +46,7 @@ protected override void ProcessNewLines(IEnumerable lines) // after all that. foreach (string line in lines) { - if (string.IsNullOrEmpty(line) || line.StartsWith("#") || line.StartsWith("$")) + if (string.IsNullOrEmpty(line) || line.StartsWith('#') || line.StartsWith('$')) { continue; } diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/InfoOutputReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/InfoOutputReceiver.cs index f7c0e213..f99485af 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/InfoOutputReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/InfoOutputReceiver.cs @@ -4,32 +4,48 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Processes command line output of a adb shell command. /// public class InfoOutputReceiver : MultiLineReceiver { + /// + /// Initializes a new instance of the class. + /// + public InfoOutputReceiver() { } + /// /// Gets or sets a dictionary with the extracted properties and their corresponding values. /// - private Dictionary Properties { get; set; } = new Dictionary(); + private Dictionary Properties { get; set; } = []; /// /// Gets or sets the dictionary with all properties and their corresponding property parsers. /// A property parser extracts the property value out of a if possible. /// - private Dictionary> PropertyParsers { get; set; } = new Dictionary>(); + private Dictionary> PropertyParsers { get; set; } = []; /// /// Gets the value of the property out of the Properties dictionary. /// Returns null if the property is not present in the directory. /// - /// The name of the property + /// The name of the property. /// The received value - public object GetPropertyValue(string propertyName) => Properties.TryGetValue(propertyName, out object property) ? property : null; + public object? GetPropertyValue(string propertyName) => Properties.TryGetValue(propertyName, out object? property) ? property : null; + + /// + /// Gets the value of the property out of the Properties dictionary. + /// Returns null if the property is not present in the directory. + /// + /// The type of the property + /// The name of the property. + /// The received value. + [return: MaybeNull] + public T GetPropertyValue(string propertyName) => Properties.TryGetValue(propertyName, out object? property) && property is T value ? value : default; /// /// Adds a new parser to this receiver. @@ -38,7 +54,7 @@ public class InfoOutputReceiver : MultiLineReceiver /// /// The property corresponding with the parser. /// Function parsing one string and returning the property value if possible. - public void AddPropertyParser(string property, Func parser) => PropertyParsers[property] = parser; + public void AddPropertyParser(string property, Func parser) => PropertyParsers[property] = parser; /// /// Processes the new lines, and sets version information if the line represents package information data. @@ -53,9 +69,9 @@ protected override void ProcessNewLines(IEnumerable lines) continue; } - foreach (KeyValuePair> parser in PropertyParsers) + foreach (KeyValuePair> parser in PropertyParsers) { - object propertyValue = parser.Value(line); + object? propertyValue = parser.Value(line); if (propertyValue != null) { Properties[parser.Key] = propertyValue; diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/InstallOutputReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/InstallOutputReceiver.cs index 8e6a8a68..29e91c98 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/InstallOutputReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/InstallOutputReceiver.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Processes output of the pm install command. @@ -42,15 +42,20 @@ public partial class InstallOutputReceiver : MultiLineReceiver /// private const string ErrorPattern = @"Error:\s+(.*)?"; + /// + /// Initializes a new instance of the class. + /// + public InstallOutputReceiver() { } + /// /// Gets the error message if the install was unsuccessful. /// - public string ErrorMessage { get; private set; } + public string? ErrorMessage { get; private set; } /// /// Gets the success message if the install is successful. /// - public string SuccessMessage { get; private set; } + public string? SuccessMessage { get; private set; } /// /// Gets a value indicating whether the install was a success. @@ -99,7 +104,7 @@ protected override void ProcessNewLines(IEnumerable lines) if (m.Success) { string msg = m.Groups[1].Value; - ErrorMessage = msg.IsNullOrWhiteSpace() ? UnknownError : msg; + ErrorMessage = StringExtensions.IsNullOrWhiteSpace(msg) ? UnknownError : msg; } Success = false; @@ -114,7 +119,7 @@ protected override void ProcessNewLines(IEnumerable lines) if (m.Success) { string msg = m.Groups[1].Value; - ErrorMessage = msg.IsNullOrWhiteSpace() ? UnknownError : msg; + ErrorMessage = StringExtensions.IsNullOrWhiteSpace(msg) ? UnknownError : msg; } Success = false; diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/NamespaceDoc.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/NamespaceDoc.cs new file mode 100644 index 00000000..8c2de411 --- /dev/null +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/NamespaceDoc.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands +{ + /// + /// The classes in this namespace provide receivers for . + /// + /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. + [CompilerGenerated] + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : Receivers.NamespaceDoc { } +} diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/PackageManagerReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/PackageManagerReceiver.cs index 9cc921dc..9ea82c3b 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/PackageManagerReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/PackageManagerReceiver.cs @@ -4,33 +4,23 @@ using System.Collections.Generic; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Parses the output of the various pm commands. /// - public class PackageManagerReceiver : MultiLineReceiver + /// The parent package manager. + public class PackageManagerReceiver(PackageManager packageManager) : MultiLineReceiver { - /// - /// Initializes a new instance of the class. - /// - /// The device for which the package information is being received. - /// The parent package manager. - public PackageManagerReceiver(DeviceData device, PackageManager packageManager) - { - Device = device; - PackageManager = packageManager; - } - /// /// Gets the device. /// - public DeviceData Device { get; private set; } + public DeviceData Device => PackageManager.Device; /// /// Gets the package manager. /// - public PackageManager PackageManager { get; private set; } + public PackageManager PackageManager { get; } = packageManager; /// /// Processes the new lines. @@ -63,7 +53,7 @@ protected override void ProcessNewLines(IEnumerable lines) if (separator == -1) { - PackageManager.Packages[package] = null; + PackageManager.Packages[package] = string.Empty; } else { diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/ProcessOutputReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/ProcessOutputReceiver.cs index 136adb4a..eb459f7f 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/ProcessOutputReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/ProcessOutputReceiver.cs @@ -2,21 +2,24 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Parses the output of a cat /proc/[pid]/stat command. /// public class ProcessOutputReceiver : MultiLineReceiver { + /// + /// Initializes a new instance of the class. + /// + public ProcessOutputReceiver() { } + /// /// Gets a list of all processes that have been received. /// - public Collection Processes { get; private set; } = new Collection(); + public List Processes { get; } = []; /// protected override void ProcessNewLines(IEnumerable lines) @@ -31,9 +34,9 @@ protected override void ProcessNewLines(IEnumerable lines) try { - Processes.Add(AndroidProcess.Parse(line, cmdLinePrefix: true)); + Processes.Add(new AndroidProcess(line, cmdLinePrefix: true)); } - catch (Exception) + catch { // Swallow } diff --git a/AdvancedSharpAdbClient/DeviceCommands/Receivers/VersionInfoReceiver.cs b/AdvancedSharpAdbClient/DeviceCommands/Receivers/VersionInfoReceiver.cs index a4654f6f..c9d4d37c 100644 --- a/AdvancedSharpAdbClient/DeviceCommands/Receivers/VersionInfoReceiver.cs +++ b/AdvancedSharpAdbClient/DeviceCommands/Receivers/VersionInfoReceiver.cs @@ -5,7 +5,7 @@ using System; using System.Text.RegularExpressions; -namespace AdvancedSharpAdbClient.DeviceCommands +namespace AdvancedSharpAdbClient.Receivers.DeviceCommands { /// /// Processes command line output of the dumpsys package command. @@ -45,9 +45,9 @@ public VersionInfoReceiver() /// Gets the version code of the specified package. /// public VersionInfo VersionInfo => - GetPropertyValue(versionCode) != null && GetPropertyValue(versionName) != null - ? new VersionInfo((int)GetPropertyValue(versionCode), (string)GetPropertyValue(versionName)) - : null; + GetPropertyValue(versionName) is string name + ? new VersionInfo(GetPropertyValue(versionCode), name) + : default; private void CheckPackagesSection(string line) { @@ -61,7 +61,7 @@ private void CheckPackagesSection(string line) // We check whether the line is indented. If it's not, and it's not an empty line, we take it is // a section header line and update the data accordingly. - if (line.IsNullOrWhiteSpace()) + if (StringExtensions.IsNullOrWhiteSpace(line)) { return; } @@ -79,7 +79,7 @@ private void CheckPackagesSection(string line) /// /// The line to be parsed. /// The extracted version name. - internal object GetVersionName(string line) + internal string? GetVersionName(string line) { CheckPackagesSection(line); @@ -97,7 +97,7 @@ internal object GetVersionName(string line) /// /// The line to be parsed. /// The extracted version code. - internal object GetVersionCode(string line) + internal object? GetVersionCode(string line) { CheckPackagesSection(line); diff --git a/AdvancedSharpAdbClient/DeviceMonitor.Async.cs b/AdvancedSharpAdbClient/DeviceMonitor.Async.cs index 93e19d20..181a7267 100644 --- a/AdvancedSharpAdbClient/DeviceMonitor.Async.cs +++ b/AdvancedSharpAdbClient/DeviceMonitor.Async.cs @@ -3,8 +3,8 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; using System.Threading; @@ -13,8 +13,8 @@ namespace AdvancedSharpAdbClient public partial class DeviceMonitor { /// - /// When the method is called, this - /// is used to block the method until the + /// When the method is called, this + /// is used to block the method until the /// has processed the first list of devices. /// protected readonly ManualResetEvent firstDeviceListParsed = new(false); @@ -27,32 +27,31 @@ public partial class DeviceMonitor /// /// The that monitors the and waits for device notifications. /// - protected Task monitorTask; + protected Task? monitorTask; /// + [MemberNotNull(nameof(monitorTask))] public virtual async Task StartAsync(CancellationToken cancellationToken = default) { if (monitorTask == null) { _ = firstDeviceListParsed.Reset(); - monitorTask = Utilities.Run(() => DeviceMonitorLoopAsync(monitorTaskCancellationTokenSource.Token), cancellationToken); + monitorTask = DeviceMonitorLoopAsync(monitorTaskCancellationTokenSource.Token); // Wait for the worker thread to have read the first list of devices. - _ = await Utilities.Run(firstDeviceListParsed.WaitOne, cancellationToken); + _ = await Extensions.Run(firstDeviceListParsed.WaitOne, cancellationToken).ConfigureAwait(false); } } /// /// Stops the monitoring /// - protected virtual async #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - ValueTask + protected virtual async ValueTask DisposeAsyncCore() #else - Task + protected virtual async Task DisposeAsyncCore() #endif - DisposeAsyncCore() { if (disposed) { return; } @@ -86,13 +85,11 @@ protected virtual async } /// - public async #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - ValueTask + public async ValueTask DisposeAsync() #else - Task + public async Task DisposeAsync() #endif - DisposeAsync() { await DisposeAsyncCore().ConfigureAwait(false); Dispose(disposing: false); @@ -111,6 +108,7 @@ public async protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellationToken = default) { IsRunning = true; + await Extensions.Yield(); // Set up the connection to track the list of devices. await InitializeSocketAsync(cancellationToken).ConfigureAwait(false); @@ -124,11 +122,7 @@ protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellati firstDeviceListParsed.Set(); } -#if HAS_LOGGER catch (TaskCanceledException ex) -#else - catch (TaskCanceledException) -#endif { // We get a TaskCanceledException on Windows if (cancellationToken.IsCancellationRequested) @@ -140,17 +134,11 @@ protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellati else { // The exception was unexpected, so log it & rethrow. -#if HAS_LOGGER logger.LogError(ex, ex.Message); -#endif throw; } } -#if HAS_LOGGER catch (ObjectDisposedException ex) -#else - catch (ObjectDisposedException) -#endif { // ... but an ObjectDisposedException on .NET Core App on Linux and macOS. if (cancellationToken.IsCancellationRequested) @@ -162,17 +150,11 @@ protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellati else { // The exception was unexpected, so log it & rethrow. -#if HAS_LOGGER logger.LogError(ex, ex.Message); -#endif throw; } } -#if HAS_LOGGER catch (OperationCanceledException ex) -#else - catch (OperationCanceledException) -#endif { // ... and an OperationCanceledException on .NET Core App 2.1 or greater. if (cancellationToken.IsCancellationRequested) @@ -184,9 +166,7 @@ protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellati else { // The exception was unexpected, so log it & rethrow. -#if HAS_LOGGER logger.LogError(ex, ex.Message); -#endif throw; } } @@ -204,36 +184,27 @@ protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellati { // The adb server was killed, for whatever reason. Try to restart it and recover from this. await AdbServer.Instance.RestartServerAsync(cancellationToken).ConfigureAwait(false); - Socket.Reconnect(); + Socket.Reconnect(false); await InitializeSocketAsync(cancellationToken).ConfigureAwait(false); } else { // The exception was unexpected, so log it & rethrow. -#if HAS_LOGGER logger.LogError(ex, ex.Message); -#endif throw ex; } } else { // The exception was unexpected, so log it & rethrow. -#if HAS_LOGGER logger.LogError(adbException, adbException.Message); -#endif throw; } } -#if HAS_LOGGER catch (Exception ex) { // The exception was unexpected, so log it & rethrow. logger.LogError(ex, ex.Message); -#else - catch (Exception) - { -#endif throw; } } @@ -243,8 +214,8 @@ protected virtual async Task DeviceMonitorLoopAsync(CancellationToken cancellati private async Task InitializeSocketAsync(CancellationToken cancellationToken) { // Set up the connection to track the list of devices. - await Socket.SendAdbRequestAsync("host:track-devices", cancellationToken); - _ = await Socket.ReadAdbResponseAsync(cancellationToken); + await Socket.SendAdbRequestAsync("host:track-devices", cancellationToken).ConfigureAwait(false); + _ = await Socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } } } diff --git a/AdvancedSharpAdbClient/DeviceMonitor.cs b/AdvancedSharpAdbClient/DeviceMonitor.cs index fa8605c9..dd2bb661 100644 --- a/AdvancedSharpAdbClient/DeviceMonitor.cs +++ b/AdvancedSharpAdbClient/DeviceMonitor.cs @@ -2,11 +2,12 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net; using System.Net.Sockets; using System.Threading; @@ -34,14 +35,14 @@ namespace AdvancedSharpAdbClient /// public partial class DeviceMonitor : IDeviceMonitor { + private static readonly char[] separator = Extensions.NewLineSeparator; + private bool disposed = false; -#if HAS_LOGGER /// /// The logger to use when logging messages. /// protected readonly ILogger logger; -#endif /// /// The list of devices currently connected to the Android Debug Bridge. @@ -70,62 +71,79 @@ public partial class DeviceMonitor : IDeviceMonitor /// /// The that monitors the and waits for device notifications. /// - protected Thread monitorThread; + protected Thread? monitorThread; #endif -#if !HAS_LOGGER -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif + /// + public event EventHandler? DeviceChanged; + + /// + public event EventHandler? DeviceNotified; + + /// + public event EventHandler? DeviceConnected; + + /// + public event EventHandler? DeviceListChanged; + + /// + public event EventHandler? DeviceDisconnected; + + /// + /// Initializes a new instance of the class. + /// + /// The logger to use when logging. + public DeviceMonitor(ILogger? logger = null) + : this(Factories.AdbSocketFactory(new IPEndPoint(IPAddress.Loopback, AdbClient.AdbServerPort)), logger) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The at which the adb server is listening. + /// The logger to use when logging. + public DeviceMonitor(EndPoint endPoint, ILogger? logger = null) + : this(Factories.AdbSocketFactory(endPoint), logger) + { + } + /// /// Initializes a new instance of the class. /// /// The that manages the connection with the adb server. /// The logger to use when logging. - public DeviceMonitor(IAdbSocket socket -#if HAS_LOGGER - , ILogger logger = null -#endif - ) + public DeviceMonitor(IAdbSocket socket, ILogger? logger = null) { Socket = socket ?? throw new ArgumentNullException(nameof(socket)); - devices = new List(); + devices = []; Devices = devices.AsReadOnly(); -#if HAS_LOGGER - this.logger = logger ?? NullLogger.Instance; -#endif + this.logger = logger ?? LoggerProvider.CreateLogger(); } -#if !HAS_LOGGER -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif /// - public event EventHandler DeviceChanged; - - /// - public event EventHandler DeviceNotified; - - /// - public event EventHandler DeviceConnected; - - /// - public event EventHandler DeviceDisconnected; - - /// - public ReadOnlyCollection Devices { get; private set; } + public ReadOnlyCollection Devices { get; init; } /// /// Gets the that represents the connection to the /// Android Debug Bridge. /// - public IAdbSocket Socket { get; private set; } + public IAdbSocket Socket { get; protected set; } /// /// Gets a value indicating whether this instance is running. /// /// if this instance is running; otherwise, . - public bool IsRunning { get; private set; } + public bool IsRunning { get; protected set; } /// + [MemberNotNull( +#if HAS_TASK + nameof(monitorTask) +#else + nameof(monitorThread) +#endif + )] public virtual void Start() { #if HAS_TASK @@ -198,7 +216,7 @@ protected virtual void Dispose(bool disposing) // Stop the thread. The tread will keep waiting for updated information from adb // eternally, so we need to forcefully abort it here. isMonitorThreadCancel = true; - Socket?.Dispose(); + Socket?.Close(); _ = monitorLoopFinished.WaitOne(); monitorThread = null; @@ -223,30 +241,6 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnDeviceChanged(DeviceDataChangeEventArgs e) => DeviceChanged?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnDeviceNotified(DeviceDataNotifyEventArgs e) => DeviceNotified?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnDeviceConnected(DeviceDataConnectEventArgs e) => DeviceConnected?.Invoke(this, e); - - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnDeviceDisconnected(DeviceDataConnectEventArgs e) => DeviceDisconnected?.Invoke(this, e); - #if !HAS_TASK /// /// Monitors the devices. This connects to the Debug Bridge @@ -284,16 +278,20 @@ protected virtual void DeviceMonitorLoop() { // The adb server was killed, for whatever reason. Try to restart it and recover from this. AdbServer.Instance.RestartServer(); - Socket.Reconnect(); + Socket.Reconnect(false); InitializeSocket(); } else { + // The exception was unexpected, so log it & rethrow. + logger.LogError(ex, ex.Message); throw ex; } } else { + // The exception was unexpected, so log it & rethrow. + logger.LogError(adbException, adbException.Message); throw; } } @@ -320,18 +318,18 @@ private void InitializeSocket() /// private void ProcessIncomingDeviceData(string result) { - string[] deviceValues = result.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - - IEnumerable currentDevices = deviceValues.Select(DeviceData.CreateFromAdbData); + string[] deviceValues = result.Split(separator, StringSplitOptions.RemoveEmptyEntries); + IEnumerable currentDevices = deviceValues.Select(x => new DeviceData(x)); UpdateDevices(currentDevices); + DeviceNotified?.Invoke(this, new DeviceDataNotifyEventArgs(currentDevices)); } /// /// Processes the incoming . /// - protected virtual void UpdateDevices(IEnumerable devices) + protected virtual void UpdateDevices(IEnumerable collection) { - lock (this.devices) + lock (devices) { // For each device in the current list, we look for a matching the new list. // * if we find it, we update the current object with whatever new information @@ -342,34 +340,47 @@ protected virtual void UpdateDevices(IEnumerable devices) // Once this is done, the new list contains device we aren't monitoring yet, so we // add them to the list, and start monitoring them. - // Add or update existing devices - foreach (DeviceData device in devices) + bool isChanged = false; + List devices = collection.ToList(); + for (int i = this.devices.Count; --i >= 0;) { - DeviceData existingDevice = Devices.SingleOrDefault(d => d.Serial == device.Serial); - - if (existingDevice == null) + DeviceData currentDevice = this.devices[i]; + int index = devices.FindIndex(d => d.Serial == currentDevice.Serial); + if (index == -1) { - this.devices.Add(device); - OnDeviceConnected(new DeviceDataConnectEventArgs(device, true)); + // Remove disconnected devices + this.devices.RemoveAt(i); + DeviceDisconnected?.Invoke(this, new DeviceDataConnectEventArgs(currentDevice, false)); + isChanged = true; } - else if (existingDevice.State != device.State) + else { - DeviceState oldState = existingDevice.State; - existingDevice.State = device.State; - OnDeviceChanged(new DeviceDataChangeEventArgs(existingDevice, device.State, oldState)); + DeviceData device = devices[index]; + if (currentDevice.State != device.State) + { + // Change device state + this.devices[i] = device; + DeviceChanged?.Invoke(this, new DeviceDataChangeEventArgs(device, device.State, currentDevice.State)); + isChanged = true; + } + devices.RemoveAt(index); } } - // Remove devices - foreach (DeviceData device in Devices.Where(d => !devices.Any(e => e.Serial == d.Serial)).ToArray()) + if (devices.Count > 0) { - this.devices.Remove(device); - OnDeviceDisconnected(new DeviceDataConnectEventArgs(device, false)); + // Add connected devices + foreach (DeviceData device in devices) + { + this.devices.Add(device); + DeviceConnected?.Invoke(this, new DeviceDataConnectEventArgs(device, false)); + } + isChanged = true; } - if (devices.Any()) + if (isChanged) { - OnDeviceNotified(new DeviceDataNotifyEventArgs(devices)); + DeviceListChanged?.Invoke(this, new DeviceDataNotifyEventArgs(devices)); } } } diff --git a/AdvancedSharpAdbClient/Exceptions/AdbException.cs b/AdvancedSharpAdbClient/Exceptions/AdbException.cs index fd38c129..6f3dc0b4 100644 --- a/AdvancedSharpAdbClient/Exceptions/AdbException.cs +++ b/AdvancedSharpAdbClient/Exceptions/AdbException.cs @@ -10,6 +10,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Represents an exception with communicating with ADB. /// + [Serializable] public class AdbException : Exception { /// @@ -23,7 +24,7 @@ public AdbException() : base("An error occurred with ADB") /// Initializes a new instance of the class with the specified error message. /// /// The message that describes the error. - public AdbException(string message) : base(message) + public AdbException(string? message) : base(message) { } @@ -32,19 +33,28 @@ public AdbException(string message) : base(message) /// /// The message that describes the error on the client side. /// The raw error message that was sent by adb. - public AdbException(string message, string adbError) : base(message) => AdbError = adbError; + public AdbException(string? message, string? adbError) : base(message) => AdbError = adbError; /// /// Initializes a new instance of the class with the specified client error message and /// /// The message that describes the error on the client side. /// The that was sent by adb. - public AdbException(string message, AdbResponse response) : base(message) + public AdbException(string? message, AdbResponse response) : base(message) { AdbError = response.Message; Response = response; } + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public AdbException(string? message, Exception? innerException) : base(message, innerException) + { + } + #if HAS_SERIALIZATION /// /// Initializes a new instance of the class. @@ -59,24 +69,15 @@ public AdbException(SerializationInfo serializationInfo, StreamingContext contex } #endif - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The inner exception. - public AdbException(string message, Exception innerException) : base(message, innerException) - { - } - /// /// Gets the error message that was sent by adb. /// - public string AdbError { get; private set; } + public string? AdbError { get; init; } /// /// Gets the response that was sent by adb. /// - public AdbResponse Response { get; private set; } + public AdbResponse Response { get; init; } /// /// Gets a value indicating whether the underlying error was a where the @@ -84,6 +85,7 @@ public AdbException(string message, Exception innerException) : base(message, in /// that the connection was reset by the remote server. This happens when the adb server was killed. /// public bool ConnectionReset => - InnerException is SocketException socketException && socketException.SocketErrorCode == SocketError.ConnectionReset; + InnerException is SocketException socketException + && socketException.SocketErrorCode == SocketError.ConnectionReset; } } diff --git a/AdvancedSharpAdbClient/Exceptions/CommandAbortingException.cs b/AdvancedSharpAdbClient/Exceptions/CommandAbortingException.cs index beb8b78b..4f453ba8 100644 --- a/AdvancedSharpAdbClient/Exceptions/CommandAbortingException.cs +++ b/AdvancedSharpAdbClient/Exceptions/CommandAbortingException.cs @@ -9,6 +9,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Thrown when an executed command identifies that it is being aborted. /// + [Serializable] public class CommandAbortingException : Exception { /// @@ -22,7 +23,16 @@ public CommandAbortingException() : base("Permission to access the specified res /// Initializes a new instance of the class. /// /// The message. - public CommandAbortingException(string message) : base(message) + public CommandAbortingException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public CommandAbortingException(string? message, Exception? innerException) : base(message, innerException) { } @@ -39,14 +49,5 @@ public CommandAbortingException(SerializationInfo serializationInfo, StreamingCo { } #endif - - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The inner exception. - public CommandAbortingException(string message, Exception innerException) : base(message, innerException) - { - } } } diff --git a/AdvancedSharpAdbClient/Exceptions/DeviceNotFoundException.cs b/AdvancedSharpAdbClient/Exceptions/DeviceNotFoundException.cs index 6d00a6d3..44046194 100644 --- a/AdvancedSharpAdbClient/Exceptions/DeviceNotFoundException.cs +++ b/AdvancedSharpAdbClient/Exceptions/DeviceNotFoundException.cs @@ -9,6 +9,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Unable to connect to the device because it was not found in the list of available devices. /// + [Serializable] public class DeviceNotFoundException : AdbException { /// @@ -22,7 +23,7 @@ public DeviceNotFoundException() : base("The device was not found.") /// Initializes a new instance of the class. /// /// The device. - public DeviceNotFoundException(string device) : base("The device '" + device + "' was not found.") + public DeviceNotFoundException(string? device) : base("The device '" + device + "' was not found.") { } @@ -31,7 +32,7 @@ public DeviceNotFoundException(string device) : base("The device '" + device + " /// /// The message. /// The inner exception. - public DeviceNotFoundException(string message, Exception innerException) : base(message, innerException) + public DeviceNotFoundException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/AdvancedSharpAdbClient/Exceptions/ElementNotFoundException.cs b/AdvancedSharpAdbClient/Exceptions/ElementNotFoundException.cs index 34b5807c..55247ee3 100644 --- a/AdvancedSharpAdbClient/Exceptions/ElementNotFoundException.cs +++ b/AdvancedSharpAdbClient/Exceptions/ElementNotFoundException.cs @@ -9,13 +9,8 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Represents an exception with Element. /// - public class ElementNotFoundException : Exception + [Serializable] + public class ElementNotFoundException(string? message) : Exception(message) { - /// - /// Initializes a new instance of the class. - /// - public ElementNotFoundException(string message) : base(message) - { - } } } diff --git a/AdvancedSharpAdbClient/Exceptions/InvalidKeyEventException.cs b/AdvancedSharpAdbClient/Exceptions/InvalidKeyEventException.cs index 93f67c34..63c73e2a 100644 --- a/AdvancedSharpAdbClient/Exceptions/InvalidKeyEventException.cs +++ b/AdvancedSharpAdbClient/Exceptions/InvalidKeyEventException.cs @@ -9,13 +9,8 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Represents an exception with key event. /// - public class InvalidKeyEventException : Exception + [Serializable] + public class InvalidKeyEventException(string? message) : Exception(message) { - /// - /// Initializes a new instance of the class. - /// - public InvalidKeyEventException(string message) : base(message) - { - } } } diff --git a/AdvancedSharpAdbClient/Exceptions/InvalidTextException.cs b/AdvancedSharpAdbClient/Exceptions/InvalidTextException.cs index 44b0ad5f..a7518943 100644 --- a/AdvancedSharpAdbClient/Exceptions/InvalidTextException.cs +++ b/AdvancedSharpAdbClient/Exceptions/InvalidTextException.cs @@ -9,6 +9,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Represents an exception with Element. /// + [Serializable] public class InvalidTextException : Exception { /// diff --git a/AdvancedSharpAdbClient/Exceptions/JavaException.cs b/AdvancedSharpAdbClient/Exceptions/JavaException.cs index aae34213..2da3e4d1 100644 --- a/AdvancedSharpAdbClient/Exceptions/JavaException.cs +++ b/AdvancedSharpAdbClient/Exceptions/JavaException.cs @@ -8,6 +8,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Represents an exception with the Java exception output. /// + [Serializable] public partial class JavaException : Exception { private const string UnknownError = "An error occurred in Java"; @@ -15,22 +16,24 @@ public partial class JavaException : Exception private const string ExceptionOutput = "java.lang."; private const string ExceptionPattern = @"java.lang.(\w+Exception):\s+(.*)?"; + private static readonly char[] separator = Extensions.NewLineSeparator; + /// /// Gets the name of Java exception. /// - public string JavaName { get; } + public string? JavaName { get; init; } /// /// Gets a string representation of the immediate frames on the call stack of Java exception. /// - public string JavaStackTrace { get; } + public string? JavaStackTrace { get; init; } /// /// Initializes a new instance of the class. /// /// The name of Java exception. /// The stackTrace of Java exception. - public JavaException(string name, string stackTrace) : base(UnknownError) + public JavaException(string? name, string? stackTrace) : base(UnknownError) { JavaName = name; JavaStackTrace = stackTrace; @@ -42,7 +45,20 @@ public JavaException(string name, string stackTrace) : base(UnknownError) /// The name of Java exception. /// The message of Java exception. /// The stackTrace of Java exception. - public JavaException(string name, string message, string stackTrace) : base(message) + public JavaException(string? name, string? message, string? stackTrace) : base(message) + { + JavaName = name; + JavaStackTrace = stackTrace; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of Java exception. + /// The message of Java exception. + /// The stackTrace of Java exception. + /// The inner exception. + public JavaException(string? name, string? message, string? stackTrace, Exception? innerException) : base(message, innerException) { JavaName = name; JavaStackTrace = stackTrace; @@ -62,25 +78,12 @@ public JavaException(SerializationInfo serializationInfo, StreamingContext conte } #endif - /// - /// Initializes a new instance of the class. - /// - /// The name of Java exception. - /// The message of Java exception. - /// The stackTrace of Java exception. - /// The inner exception. - public JavaException(string name, string message, string stackTrace, Exception innerException) : base(message, innerException) - { - JavaName = name; - JavaStackTrace = stackTrace; - } - /// /// Creates a from it representation. /// /// A which represents a . /// The equivalent . - public static JavaException Parse(string line) => Parse(line.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)); + public static JavaException Parse(string line) => Parse(line.Split(separator, StringSplitOptions.RemoveEmptyEntries)); /// @@ -106,10 +109,10 @@ public static JavaException Parse(IEnumerable lines) exception = m.Groups[1].Value; message = m.Groups[2].Value; - message = message.IsNullOrWhiteSpace() ? UnknownError : message; + message = StringExtensions.IsNullOrWhiteSpace(message) ? UnknownError : message; } } - else if (!line.IsNullOrWhiteSpace()) + else if (!StringExtensions.IsNullOrWhiteSpace(line)) { stackTrace.AppendLine(line.TrimEnd()); } diff --git a/AdvancedSharpAdbClient/Exceptions/NamespaceDoc.cs b/AdvancedSharpAdbClient/Exceptions/NamespaceDoc.cs index dfa29824..bf3b8d99 100644 --- a/AdvancedSharpAdbClient/Exceptions/NamespaceDoc.cs +++ b/AdvancedSharpAdbClient/Exceptions/NamespaceDoc.cs @@ -3,6 +3,7 @@ // using System; +using System.ComponentModel; using System.Runtime.CompilerServices; namespace AdvancedSharpAdbClient.Exceptions @@ -12,5 +13,6 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. [CompilerGenerated] - internal class NamespaceDoc { } + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : AdvancedSharpAdbClient.NamespaceDoc { } } diff --git a/AdvancedSharpAdbClient/Exceptions/PackageInstallationException.cs b/AdvancedSharpAdbClient/Exceptions/PackageInstallationException.cs index 76d3c9fa..2878805c 100644 --- a/AdvancedSharpAdbClient/Exceptions/PackageInstallationException.cs +++ b/AdvancedSharpAdbClient/Exceptions/PackageInstallationException.cs @@ -9,9 +9,7 @@ namespace AdvancedSharpAdbClient.DeviceCommands /// /// An exception while installing a package on the device. /// -#if HAS_SERIALIZATION [Serializable] -#endif public class PackageInstallationException : Exception { /// @@ -25,7 +23,7 @@ public PackageInstallationException() /// Initializes a new instance of the class. /// /// The message. - public PackageInstallationException(string message) : base(message) + public PackageInstallationException(string? message) : base(message) { } @@ -34,7 +32,7 @@ public PackageInstallationException(string message) : base(message) /// /// The message. /// The inner. - public PackageInstallationException(string message, Exception inner) : base(message, inner) + public PackageInstallationException(string? message, Exception? inner) : base(message, inner) { } diff --git a/AdvancedSharpAdbClient/Exceptions/PermissionDeniedException.cs b/AdvancedSharpAdbClient/Exceptions/PermissionDeniedException.cs index fd7ae804..1fa9d0b0 100644 --- a/AdvancedSharpAdbClient/Exceptions/PermissionDeniedException.cs +++ b/AdvancedSharpAdbClient/Exceptions/PermissionDeniedException.cs @@ -9,6 +9,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// The exception that is thrown when the permission to a resource was denied. /// + [Serializable] public class PermissionDeniedException : Exception { /// @@ -22,7 +23,16 @@ public PermissionDeniedException() : base("Permission to access the specified re /// Initializes a new instance of the class. /// /// The message. - public PermissionDeniedException(string message) : base(message) + public PermissionDeniedException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public PermissionDeniedException(string? message, Exception? innerException) : base(message, innerException) { } @@ -39,14 +49,5 @@ public PermissionDeniedException(SerializationInfo serializationInfo, StreamingC { } #endif - - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The inner exception. - public PermissionDeniedException(string message, Exception innerException) : base(message, innerException) - { - } } } diff --git a/AdvancedSharpAdbClient/Exceptions/ShellCommandUnresponsiveException.cs b/AdvancedSharpAdbClient/Exceptions/ShellCommandUnresponsiveException.cs index a1e0bc93..a5bba4c7 100644 --- a/AdvancedSharpAdbClient/Exceptions/ShellCommandUnresponsiveException.cs +++ b/AdvancedSharpAdbClient/Exceptions/ShellCommandUnresponsiveException.cs @@ -9,6 +9,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// The exception that is thrown when a shell command becomes unresponsive. /// + [Serializable] public class ShellCommandUnresponsiveException : AdbException { /// @@ -23,7 +24,7 @@ public ShellCommandUnresponsiveException() : base("The shell command has become /// specified error message. /// /// The message that describes the error. - public ShellCommandUnresponsiveException(string message) : base(message) + public ShellCommandUnresponsiveException(string? message) : base(message) { } @@ -32,7 +33,7 @@ public ShellCommandUnresponsiveException(string message) : base(message) /// reference to the inner exception that is the cause of this exception. /// /// The exception that is the cause of the current exception, or if no inner exception is specified. - public ShellCommandUnresponsiveException(Exception inner) : base("The shell command has become unresponsive", inner) + public ShellCommandUnresponsiveException(Exception? inner) : base("The shell command has become unresponsive", inner) { } @@ -43,7 +44,7 @@ public ShellCommandUnresponsiveException(Exception inner) : base("The shell comm /// /// The error message that explains the reason for the exception. /// The exception that is the cause of the current exception, or if no inner exception is specified. - public ShellCommandUnresponsiveException(string message, Exception inner) : base(message, inner) + public ShellCommandUnresponsiveException(string? message, Exception? inner) : base(message, inner) { } diff --git a/AdvancedSharpAdbClient/Exceptions/UnknownOptionException.cs b/AdvancedSharpAdbClient/Exceptions/UnknownOptionException.cs index 57d2eb56..18a9c96f 100644 --- a/AdvancedSharpAdbClient/Exceptions/UnknownOptionException.cs +++ b/AdvancedSharpAdbClient/Exceptions/UnknownOptionException.cs @@ -9,6 +9,7 @@ namespace AdvancedSharpAdbClient.Exceptions /// /// Thrown when a command has an unknown option passed. /// + [Serializable] public class UnknownOptionException : Exception { /// @@ -22,7 +23,16 @@ public UnknownOptionException() : base("Unknown option.") /// Initializes a new instance of the class. /// /// The message. - public UnknownOptionException(string message) : base(message) + public UnknownOptionException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public UnknownOptionException(string? message, Exception? innerException) : base(message, innerException) { } @@ -39,14 +49,5 @@ public UnknownOptionException(SerializationInfo serializationInfo, StreamingCont { } #endif - - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The inner exception. - public UnknownOptionException(string message, Exception innerException) : base(message, innerException) - { - } } } diff --git a/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.Async.cs b/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.Async.cs index e3a9cf52..be2808eb 100644 --- a/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.Async.cs +++ b/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.Async.cs @@ -3,8 +3,10 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net; using System.Threading; @@ -12,6 +14,23 @@ namespace AdvancedSharpAdbClient { public static partial class AdbClientExtensions { + /// + /// Asks the ADB server to forward local connections from + /// to the address on the . + /// + /// An instance of a class that implements the interface. + /// The device on which to forward the connections. + /// The local address to forward. + /// The remote address to forward. + /// If set to , the request will fail if there is already a forward + /// connection from . + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + /// If your requested to start forwarding to local port TCP:0, the port number of the TCP port + /// which has been opened. In all other cases, 0. + public static Task CreateForwardAsync(this IAdbClient client, DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind, CancellationToken cancellationToken = default) => + client.CreateForwardAsync(device, local.ToString(), remote.ToString(), allowRebind, cancellationToken); + /// /// Creates a port forwarding between a local and a remote port. /// @@ -43,6 +62,120 @@ public static Task CreateForwardAsync(this IAdbClient client, DeviceData de public static Task CreateForwardAsync(this IAdbClient client, DeviceData device, int localPort, string remoteSocket, CancellationToken cancellationToken = default) => client.CreateForwardAsync(device, $"tcp:{localPort}", $"local:{remoteSocket}", true, cancellationToken); + /// + /// Asks the ADB server to reverse forward local connections from + /// to the address on the . + /// + /// An instance of a class that implements the interface. + /// The device on which to reverse forward the connections. + /// The remote address to reverse forward. + /// The local address to reverse forward. + /// If set to , the request will fail if if the specified + /// socket is already bound through a previous reverse command. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + /// If your requested to start reverse to remote port TCP:0, the port number of the TCP port + /// which has been opened. In all other cases, 0. + public static Task CreateReverseForwardAsync(this IAdbClient client, DeviceData device, ForwardSpec remote, ForwardSpec local, bool allowRebind, CancellationToken cancellationToken = default) => + client.CreateReverseForwardAsync(device, remote.ToString(), local.ToString(), allowRebind, cancellationToken); + + /// + /// Remove a reverse port forwarding between a remote and a local port. + /// + /// An instance of a class that implements the interface. + /// The device on which to remove the reverse port forwarding + /// Specification of the remote that was forwarded + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task RemoveReverseForwardAsync(this IAdbClient client, DeviceData device, ForwardSpec remote, CancellationToken cancellationToken = default) => + client.RemoveReverseForwardAsync(device, remote.ToString(), cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// Optionally, a that processes the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteServerCommandAsync(this IAdbClient client, string target, string command, IShellOutputReceiver receiver, CancellationToken cancellationToken = default) => + client.ExecuteServerCommandAsync(target, command, receiver, AdbClient.Encoding, cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteServerCommandAsync(this IAdbClient client, string target, string command, IAdbSocket socket, CancellationToken cancellationToken = default) => + client.ExecuteServerCommandAsync(target, command, socket, AdbClient.Encoding, cancellationToken); + + /// + /// Executes a command on the device. + /// + /// An instance of a class that implements the interface. + /// The command to execute. + /// The device on which to run the command. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteRemoteCommandAsync(this IAdbClient client, string command, DeviceData device, CancellationToken cancellationToken = default) => + client.ExecuteRemoteCommandAsync(command, device, AdbClient.Encoding, cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteServerCommandAsync(this IAdbClient client, string target, string command, CancellationToken cancellationToken = default) => + client.ExecuteServerCommandAsync(target, command, AdbClient.Encoding, cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// Optionally, a that processes the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteServerCommandAsync(this IAdbClient client, string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, CancellationToken cancellationToken = default) => + client.ExecuteServerCommandAsync(target, command, socket, receiver, AdbClient.Encoding, cancellationToken); + + /// + /// Executes a command on the device. + /// + /// An instance of a class that implements the interface. + /// The command to execute. + /// The device on which to run the command. + /// Optionally, a that processes the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ExecuteRemoteCommandAsync(this IAdbClient client, string command, DeviceData device, IShellOutputReceiver receiver, CancellationToken cancellationToken = default) => + client.ExecuteRemoteCommandAsync(command, device, receiver, AdbClient.Encoding, cancellationToken); + + /// + /// Asynchronously runs the event log service on a device. + /// + /// An instance of a class that implements the interface. + /// The device on which to run the event log service. + /// A callback which will receive the event log messages as they are received. + /// Optionally, the names of the logs to receive. + /// A which represents the asynchronous operation. + public static Task RunLogServiceAsync(this IAdbClient client, DeviceData device, Action messageSink, params LogId[] logNames) => + client.RunLogServiceAsync(device, messageSink, default, logNames); + /// /// Reboots the specified adb socket address. /// @@ -86,19 +219,8 @@ public static Task PairAsync(this IAdbClient client, IPEndPoint endpoint /// The pairing code. /// A which can be used to cancel the asynchronous operation. /// The results from adb. - public static Task PairAsync(this IAdbClient client, string host, string code, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - string[] values = host.Split(':'); - - return values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : client.PairAsync(new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int port) ? port : AdbClient.DefaultPort), code, cancellationToken); - } + public static Task PairAsync(this IAdbClient client, string host, string code, CancellationToken cancellationToken = default) => + client.PairAsync(Extensions.CreateDnsEndPoint(host, AdbClient.DefaultPort), code, cancellationToken); /// /// Pair with a device for secure TCP/IP communication. @@ -109,19 +231,8 @@ public static Task PairAsync(this IAdbClient client, string host, string /// The pairing code. /// A which can be used to cancel the asynchronous operation. /// The results from adb. - public static Task PairAsync(this IAdbClient client, string host, int port, string code, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - string[] values = host.Split(':'); - - return values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : client.PairAsync(new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port), code, cancellationToken); - } + public static Task PairAsync(this IAdbClient client, string host, int port, string code, CancellationToken cancellationToken = default) => + client.PairAsync(Extensions.CreateDnsEndPoint(host, port), code, cancellationToken); /// /// Connect to a device via TCP/IP. @@ -155,19 +266,96 @@ public static Task ConnectAsync(this IAdbClient client, IPEndPoint endpo /// The port of the remote device. /// A which can be used to cancel the asynchronous operation. /// A which return the results from adb. - public static Task ConnectAsync(this IAdbClient client, string host, int port = AdbClient.DefaultPort, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } + public static Task ConnectAsync(this IAdbClient client, string host, int port = AdbClient.DefaultPort, CancellationToken cancellationToken = default) => + client.ConnectAsync(Extensions.CreateDnsEndPoint(host, port), cancellationToken); + + /// + /// Asynchronously installs an Android application on an device. + /// + /// An instance of a class that implements the interface. + /// The device on which to install the application. + /// A which represents the application to install. + /// The arguments to pass to adb install. + /// A which represents the asynchronous operation. + public static Task InstallAsync(this IAdbClient client, DeviceData device, Stream apk, params string[] arguments) => client.InstallAsync(device, apk, default, arguments); + + /// + /// Asynchronously push multiple APKs to the device and install them. + /// + /// An instance of a class that implements the interface. + /// The device on which to install the application. + /// s which represents the split APKs to install. + /// The package name of the base APK to install. + /// The arguments to pass to adb install-create. + /// A which represents the asynchronous operation. + public static Task InstallMultipleAsync(this IAdbClient client, DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments) => + client.InstallMultipleAsync(device, splitAPKs, packageName, default, arguments); + + /// + /// Asynchronously push multiple APKs to the device and install them. + /// + /// An instance of a class that implements the interface. + /// The device on which to install the application. + /// A which represents the base APK to install. + /// s which represents the split APKs to install. + /// The arguments to pass to adb install-create. + /// A which represents the asynchronous operation. + public static Task InstallMultipleAsync(this IAdbClient client, DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments) => + client.InstallMultipleAsync(device, baseAPK, splitAPKs, default, arguments); - string[] values = host.Split(':'); + /// + /// Like "install", but starts an install session. + /// + /// An instance of a class that implements the interface. + /// The device on which to install the application. + /// The package name of the baseAPK to install. + /// The arguments to pass to adb install-create. + /// A which return the session ID + public static Task InstallCreateAsync(this IAdbClient client, DeviceData device, string? packageName = null, params string[] arguments) => + client.InstallCreateAsync(device, packageName, default, arguments); + + /// + /// Uninstalls an Android application on an device. + /// + /// An instance of a class that implements the interface. + /// The device on which to install the application. + /// The name of the package to uninstall. + /// The arguments to pass to adb uninstall. + /// A which represents the asynchronous operation. + public static Task UninstallAsync(this IAdbClient client, DeviceData device, string packageName, params string[] arguments) => + client.UninstallAsync(device, packageName, default, arguments); - return values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : client.ConnectAsync(new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port), cancellationToken); + /// + /// Clear the input text. The input should be in focus. Use if the element isn't focused. + /// + /// An instance of a class that implements the interface. + /// The device on which to clear the input text. + /// The length of text to clear. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static async Task ClearInputAsync(this IAdbClient client, DeviceData device, int charCount, CancellationToken cancellationToken = default) + { + await client.SendKeyEventAsync(device, "KEYCODE_MOVE_END", cancellationToken).ConfigureAwait(false); + await client.SendKeyEventAsync(device, StringExtensions.Join(" ", Enumerable.Repeat("KEYCODE_DEL", charCount)), cancellationToken).ConfigureAwait(false); } + + /// + /// Click BACK button. + /// + /// An instance of a class that implements the interface. + /// The device on which to click BACK button. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ClickBackButtonAsync(this IAdbClient client, DeviceData device, CancellationToken cancellationToken = default) => client.SendKeyEventAsync(device, "KEYCODE_BACK", cancellationToken); + + /// + /// Click HOME button. + /// + /// An instance of a class that implements the interface. + /// The device on which to click HOME button. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + public static Task ClickHomeButtonAsync(this IAdbClient client, DeviceData device, CancellationToken cancellationToken = default) => client.SendKeyEventAsync(device, "KEYCODE_HOME", cancellationToken); } } #endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.cs b/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.cs index ff149bac..7cb1158b 100644 --- a/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.cs +++ b/AdvancedSharpAdbClient/Extensions/AdbClientExtensions.cs @@ -2,8 +2,8 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Linq; using System.Net; namespace AdvancedSharpAdbClient @@ -13,6 +13,108 @@ namespace AdvancedSharpAdbClient /// public static partial class AdbClientExtensions { + /// + /// Asks the ADB server to forward local connections from + /// to the address on the . + /// + /// An instance of a class that implements the interface. + /// The device on which to forward the connections. + /// The local address to forward. + /// The remote address to forward. + /// If set to , the request will fail if there is already a forward + /// connection from . + /// If your requested to start forwarding to local port TCP:0, the port number of the TCP port + /// which has been opened. In all other cases, 0. + public static int CreateForward(this IAdbClient client, DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind) => + client.CreateForward(device, local.ToString(), remote.ToString(), allowRebind); + + /// + /// Asks the ADB server to reverse forward local connections from + /// to the address on the . + /// + /// An instance of a class that implements the interface. + /// The device on which to reverse forward the connections. + /// The remote address to reverse forward. + /// The local address to reverse forward. + /// If set to , the request will fail if if the specified socket + /// is already bound through a previous reverse command. + /// If your requested to start reverse to remote port TCP:0, the port number of the TCP port + /// which has been opened. In all other cases, 0. + public static int CreateReverseForward(this IAdbClient client, DeviceData device, ForwardSpec remote, ForwardSpec local, bool allowRebind) => + client.CreateReverseForward(device, remote.ToString(), local.ToString(), allowRebind); + + /// + /// Remove a reverse port forwarding between a remote and a local port. + /// + /// An instance of a class that implements the interface. + /// The device on which to remove the reverse port forwarding + /// Specification of the remote that was forwarded + public static void RemoveReverseForward(this IAdbClient client, DeviceData device, ForwardSpec remote) => + client.RemoveReverseForward(device, remote.ToString()); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + public static void ExecuteServerCommand(this IAdbClient client, string target, string command) => + client.ExecuteServerCommand(target, command, AdbClient.Encoding); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + public static void ExecuteServerCommand(this IAdbClient client, string target, string command, IAdbSocket socket) => + client.ExecuteServerCommand(target, command, socket, AdbClient.Encoding); + + /// + /// Executes a shell command on the device. + /// + /// An instance of a class that implements the interface. + /// The command to execute. + /// The device on which to run the command. + public static void ExecuteRemoteCommand(this IAdbClient client, string command, DeviceData device) => + client.ExecuteRemoteCommand(command, device, AdbClient.Encoding); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// Optionally, a that processes the command output. + public static void ExecuteServerCommand(this IAdbClient client, string target, string command, IShellOutputReceiver receiver) => + client.ExecuteServerCommand(target, command, receiver, AdbClient.Encoding); + + /// + /// Executes a command on the adb server. + /// + /// An instance of a class that implements the interface. + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// Optionally, a that processes the command output. + public static void ExecuteServerCommand(this IAdbClient client, string target, string command, IAdbSocket socket, IShellOutputReceiver receiver) => + client.ExecuteServerCommand(target, command, socket, receiver, AdbClient.Encoding); + + /// + /// Executes a shell command on the device. + /// + /// An instance of a class that implements the interface. + /// The command to execute. + /// The device on which to run the command. + /// Optionally, a that processes the command output. + public static void ExecuteRemoteCommand(this IAdbClient client, string command, DeviceData device, IShellOutputReceiver receiver) => + client.ExecuteRemoteCommand(command, device, receiver, AdbClient.Encoding); + /// /// Creates a port forwarding between a local and a remote port. /// @@ -78,19 +180,8 @@ public static string Pair(this IAdbClient client, IPEndPoint endpoint, string co /// The host address of the remote device. /// The pairing code. /// The results from adb. - public static string Pair(this IAdbClient client, string host, string code) - { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - string[] values = host.Split(':'); - - return values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : client.Pair(new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int port) ? port : AdbClient.DefaultPort), code); - } + public static string Pair(this IAdbClient client, string host, string code) => + client.Pair(Extensions.CreateDnsEndPoint(host, AdbClient.DefaultPort), code); /// /// Pair with a device for secure TCP/IP communication. @@ -100,19 +191,8 @@ public static string Pair(this IAdbClient client, string host, string code) /// The port of the remote device. /// The pairing code. /// The results from adb. - public static string Pair(this IAdbClient client, string host, int port, string code) - { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } - - string[] values = host.Split(':'); - - return values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : client.Pair(new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port), code); - } + public static string Pair(this IAdbClient client, string host, int port, string code) => + client.Pair(Extensions.CreateDnsEndPoint(host, port), code); /// /// Connect to a device via TCP/IP. @@ -143,18 +223,33 @@ public static string Connect(this IAdbClient client, IPEndPoint endpoint) => /// The host address of the remote device. /// The port of the remote device. /// The results from adb. - public static string Connect(this IAdbClient client, string host, int port = AdbClient.DefaultPort) + public static string Connect(this IAdbClient client, string host, int port = AdbClient.DefaultPort) => + client.Connect(Extensions.CreateDnsEndPoint(host, port)); + + /// + /// Clear the input text. The input should be in focus. Use if the element isn't focused. + /// + /// An instance of a class that implements the interface. + /// The device on which to clear the input text. + /// The length of text to clear. + public static void ClearInput(this IAdbClient client, DeviceData device, int charCount) { - if (string.IsNullOrEmpty(host)) - { - throw new ArgumentNullException(nameof(host)); - } + client.SendKeyEvent(device, "KEYCODE_MOVE_END"); + client.SendKeyEvent(device, StringExtensions.Join(" ", Enumerable.Repeat("KEYCODE_DEL", charCount))); + } - string[] values = host.Split(':'); + /// + /// Click BACK button. + /// + /// An instance of a class that implements the interface. + /// The device on which to click BACK button. + public static void ClickBackButton(this IAdbClient client, DeviceData device) => client.SendKeyEvent(device, "KEYCODE_BACK"); - return values.Length <= 0 - ? throw new ArgumentNullException(nameof(host)) - : client.Connect(new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port)); - } + /// + /// Click HOME button. + /// + /// An instance of a class that implements the interface. + /// The device on which to click HOME button. + public static void ClickHomeButton(this IAdbClient client, DeviceData device) => client.SendKeyEvent(device, "KEYCODE_HOME"); } } diff --git a/AdvancedSharpAdbClient/Extensions/AdbCommandLineClientExtensions.cs b/AdvancedSharpAdbClient/Extensions/AdbCommandLineClientExtensions.cs deleted file mode 100644 index 52d7a4da..00000000 --- a/AdvancedSharpAdbClient/Extensions/AdbCommandLineClientExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using AdvancedSharpAdbClient.Exceptions; -using System.IO; - -namespace AdvancedSharpAdbClient -{ - /// - /// Provides extension methods for the class. - /// - public static class AdbCommandLineClientExtensions - { - /// - /// Throws an error if the path does not point to a valid instance of adb.exe. - /// - /// An instance of a class that implements the interface. - /// The path to validate. - public static void EnsureIsValidAdbFile(this IAdbCommandLineClient client, string adbPath) - { - ExceptionExtensions.ThrowIfNull(client); - - if (!client.IsValidAdbFile(adbPath)) - { - throw new FileNotFoundException($"The adb.exe executable could not be found at {adbPath}"); - } - } - } -} diff --git a/AdvancedSharpAdbClient/Extensions/CrossPlatformFunc.cs b/AdvancedSharpAdbClient/Extensions/CrossPlatformFunc.cs deleted file mode 100644 index af5c80a6..00000000 --- a/AdvancedSharpAdbClient/Extensions/CrossPlatformFunc.cs +++ /dev/null @@ -1,121 +0,0 @@ -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; - -namespace AdvancedSharpAdbClient -{ - /// - /// The functions which are used by the class, but which are platform-specific. - /// - public static class CrossPlatformFunc - { - /// - /// Determines whether the specified file exists. - /// - public static Func CheckFileExists { get; set; } = File.Exists; - - /// - /// Runs process, invoking a specific command, and reads the standard output and standard error output. - /// - /// The return code of the process. - public static Func, List, int> RunProcess { get; set; } = (string filename, string command, List errorOutput, List standardOutput) => - { -#if HAS_PROCESS - ProcessStartInfo psi = new(filename, command) - { - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true - }; - - using Process process = Process.Start(psi); - string standardErrorString = process.StandardError.ReadToEnd(); - string standardOutputString = process.StandardOutput.ReadToEnd(); - - errorOutput?.AddRange(standardErrorString.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)); - - standardOutput?.AddRange(standardOutputString.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)); - - // get the return code from the process - if (!process.WaitForExit(5000)) - { - process.Kill(); - } - - return process.ExitCode; -#else - throw new PlatformNotSupportedException(); -#endif - }; - -#if HAS_TASK -#if NETFRAMEWORK && !NET40_OR_GREATER - /// - /// Encapsulates a method that has five parameters and returns a value of the type specified by the parameter. - /// - /// The return value of the method that this delegate encapsulates. - public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5); -#endif - - /// - /// Runs process, invoking a specific command, and reads the standard output and standard error output. - /// - /// The return code of the process. - public static Func, List, CancellationToken, Task> RunProcessAsync { get; set; } = -#if HAS_PROCESS - async -#endif - (string filename, string command, List errorOutput, List standardOutput, CancellationToken cancellationToken) => - { -#if HAS_PROCESS - ProcessStartInfo psi = new(filename, command) - { - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true - }; - - using Process process = Process.Start(psi); - string standardErrorString = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - string standardOutputString = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - - errorOutput?.AddRange(standardErrorString.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)); - - standardOutput?.AddRange(standardOutputString.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)); - -#if NET5_0_OR_GREATER - using (CancellationTokenSource completionSource = new(TimeSpan.FromMilliseconds(5000))) - { - await process.WaitForExitAsync(completionSource.Token); - if (!process.HasExited) - { - process.Kill(); - } - } -#else - // get the return code from the process - if (!process.WaitForExit(5000)) - { - process.Kill(); - } -#endif - return process.ExitCode; -#else - TaskCompletionSource source = new(); - source.SetException(new PlatformNotSupportedException()); - return source.Task; -#endif - }; -#endif - } -} diff --git a/AdvancedSharpAdbClient/Extensions/DateTimeHelper.cs b/AdvancedSharpAdbClient/Extensions/DateTimeHelper.cs deleted file mode 100644 index ee2a24a6..00000000 --- a/AdvancedSharpAdbClient/Extensions/DateTimeHelper.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using System; - -namespace AdvancedSharpAdbClient -{ - /// - /// Provides helper methods for working with Unix-based date formats. - /// - public static class DateTimeHelper - { - /// - /// Gets EPOCH time. - /// - public static DateTime Epoch { get; } = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - /// - /// Converts a to the Unix equivalent. - /// - /// The date to convert to the Unix format. - /// A that represents the date, in Unix format. - public static long ToUnixEpoch(this DateTime date) - { - TimeSpan t = date.ToUniversalTime() - Epoch; - long epoch = (long)t.TotalSeconds; - return epoch; - } - - /// - /// Converts a Unix equivalent to the . - /// - /// The Unix equivalent to convert to the date. - /// A that represents the date. - public static DateTime ToDateTime(this long time) => Epoch.Add(new TimeSpan(time * 1000_0000)); - } -} diff --git a/AdvancedSharpAdbClient/Extensions/EnumerableBuilder.cs b/AdvancedSharpAdbClient/Extensions/EnumerableBuilder.cs new file mode 100644 index 00000000..e4ceecf9 --- /dev/null +++ b/AdvancedSharpAdbClient/Extensions/EnumerableBuilder.cs @@ -0,0 +1,34 @@ +#if HAS_BUFFERS +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; +using System.ComponentModel; + +namespace AdvancedSharpAdbClient +{ + /// + /// A collection builder provide for collection expressions. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class EnumerableBuilder + { + /// + /// Build a struct. + /// + /// The data that feeds the struct. + /// A new instance of struct. + public static ColorData ColorDataCreate(ReadOnlySpan values) => + new((uint)(values[0] | (values[1] << 8) | (values[2] << 16) | (values[3] << 24)), + (uint)(values[4] | (values[5] << 8) | (values[6] << 16) | (values[7] << 24))); + + /// + /// Build a struct. + /// + /// The data that feeds the struct. + /// A new instance of struct. + public static FramebufferHeader FramebufferHeaderCreate(ReadOnlySpan values) => new(values); + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/Extensions.cs b/AdvancedSharpAdbClient/Extensions/Extensions.cs new file mode 100644 index 00000000..487f5ac0 --- /dev/null +++ b/AdvancedSharpAdbClient/Extensions/Extensions.cs @@ -0,0 +1,249 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +#if NET40 +using Microsoft.Runtime.CompilerServices; +#endif + +namespace AdvancedSharpAdbClient +{ + /// + /// Extension methods for the namespace. + /// + internal static class Extensions + { + public static char[] NewLineSeparator { get; } = ['\r', '\n']; + + /// + /// Creates a from the specified host and port information. + /// + /// The host address. + /// The port. + /// The created from the specified host and port information. + public static DnsEndPoint CreateDnsEndPoint(string host, int port) + { + if (string.IsNullOrEmpty(host)) + { + throw new ArgumentNullException(nameof(host)); + } + + string[] values = host.Split(':'); + + return values.Length <= 0 + ? throw new ArgumentNullException(nameof(host)) + : new DnsEndPoint(values[0], values.Length > 1 && int.TryParse(values[1], out int _port) ? _port : port); + } + +#if HAS_TASK +#if NETFRAMEWORK && !NET46_OR_GREATER + /// + /// Singleton cached task that's been completed successfully. + /// + internal static readonly Task s_cachedCompleted = +#if NET45_OR_GREATER + Task. +#else + TaskEx. +#endif + FromResult(null); + + /// + /// Gets a task that's already been completed successfully. + /// + public static Task CompletedTask => s_cachedCompleted; +#else + public static Task CompletedTask => Task.CompletedTask; +#endif + + /// + /// Creates a task that completes after a specified number of milliseconds. + /// + /// The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the time delay. + /// The argument is less than -1. + public static Task Delay(int dueTime, CancellationToken cancellationToken = default) => +#if NETFRAMEWORK && !NET45_OR_GREATER + TaskEx +#else + Task +#endif + .Delay(dueTime, cancellationToken); + + /// + /// Queues the specified work to run on the thread pool and returns a proxy for the + /// returned by function. A cancellation token allows the work to be cancelled if it has not yet started. + /// + /// The type of the result returned by the proxy task. + /// The work to execute asynchronously. + /// A cancellation token that can be used to cancel the work if it has not yet started. + /// A that represents a proxy for the + /// returned by . + /// The parameter was . + public static Task Run(Func function, CancellationToken cancellationToken = default) => +#if NETFRAMEWORK && !NET45_OR_GREATER + TaskEx +#else + Task +#endif + .Run(function, cancellationToken); + + /// + /// Creates a task that will complete when all of the objects in an enumerable collection have completed. + /// + /// The tasks to wait on for completion. + /// A task that represents the completion of all of the supplied tasks. + public static Task WhenAll(IEnumerable tasks) => +#if NETFRAMEWORK && !NET45_OR_GREATER + TaskEx +#else + Task +#endif + .WhenAll(tasks); + + /// + /// Creates a task that will complete when all of the objects in an enumerable collection have completed. + /// + /// The type of the completed task. + /// The tasks to wait on for completion. + /// A task that represents the completion of all of the supplied tasks. + public static Task WhenAll(IEnumerable> tasks) => +#if NETFRAMEWORK && !NET45_OR_GREATER + TaskEx +#else + Task +#endif + .WhenAll(tasks); + + /// + /// Creates an awaitable task that asynchronously yields back to the current context when awaited. + /// + /// A context that, when awaited, will asynchronously transition back into the current context at the time of the await. + /// If the current is non-null, it is treated as the current context. Otherwise, the task scheduler + /// that is associated with the currently executing task is treated as the current context. + public static YieldAwaitable Yield() => +#if NETFRAMEWORK && !NET45_OR_GREATER + TaskEx +#else + Task +#endif + .Yield(); + +#if !NET7_0_OR_GREATER + /// + /// Reads a line of characters asynchronously and returns the data as a string. + /// + /// The to read a line. + /// The token to monitor for cancellation requests. + /// A value task that represents the asynchronous read operation. The value of the + /// TResult parameter contains the next line from the text reader, or is null if + /// all of the characters have been read. + public static async Task ReadLineAsync(this TextReader reader, CancellationToken cancellationToken) + { + using CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(reader.Close); +#if NET35 + await Yield(); + return reader.ReadLine(); +#else + return await reader.ReadLineAsync(); +#endif + } + + /// + /// Reads all characters from the current position to the end of the stream asynchronously and returns them as one string. + /// + /// The to read all characters. + /// The token to monitor for cancellation requests. + /// A task that represents the asynchronous read operation. The value of the TResult + /// parameter contains a string with the characters from the current position to + /// the end of the stream. + public static async Task ReadToEndAsync(this TextReader reader, CancellationToken cancellationToken) + { + using CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(reader.Close); +#if NET35 + await Yield(); + return reader.ReadToEnd(); +#else + return await reader.ReadToEndAsync(); +#endif + } +#endif +#endif + +#if !HAS_PROCESS + /// + /// Closes the connection and releases all associated resources. + /// + /// The to release. + public static void Close(this Socket socket) => socket.Dispose(); + + /// + /// Closes the and releases any system resources associated with the . + /// + /// The to release. + public static void Close(this TextReader reader) => reader.Dispose(); +#endif + +#if NETFRAMEWORK && !NET40_OR_GREATER + /// + /// Releases all resources used by the current instance of the class. + /// + /// The to release. + public static void Dispose(this Socket socket) + { + socket.Close(); + GC.SuppressFinalize(socket); + } + + /// + /// Releases all resources used by the current instance of the class. + /// + /// The to release. + public static void Dispose(this WaitHandle waitHandle) + { + waitHandle.Close(); + GC.SuppressFinalize(waitHandle); + } +#endif + + public static bool IsWindowsPlatform() => +#if HAS_RUNTIMEINFORMATION + RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#elif NETCORE + true; +#else + Environment.OSVersion.Platform + is PlatformID.Win32S + or PlatformID.Win32Windows + or PlatformID.Win32NT + or PlatformID.WinCE + or PlatformID.Xbox; +#endif + + public static bool IsUnixPlatform() => +#if HAS_RUNTIMEINFORMATION + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + || RuntimeInformation.IsOSPlatform(OSPlatform.OSX) +#if NETCOREAPP3_0_OR_GREATER + || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) +#endif + ; +#elif NETCORE + false; +#else + Environment.OSVersion.Platform + is PlatformID.Unix + or PlatformID.MacOSX; +#endif + } +} diff --git a/AdvancedSharpAdbClient/Extensions/Factories.cs b/AdvancedSharpAdbClient/Extensions/Factories.cs index 957f1f4e..538bea74 100644 --- a/AdvancedSharpAdbClient/Extensions/Factories.cs +++ b/AdvancedSharpAdbClient/Extensions/Factories.cs @@ -3,6 +3,8 @@ // using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Net; namespace AdvancedSharpAdbClient @@ -14,6 +16,11 @@ public static class Factories { static Factories() => Reset(); + /// + /// Determines whether the specified file exists. + /// + public static Func CheckFileExists { get; set; } + /// /// Gets or sets a delegate which creates a new instance of the class. /// @@ -43,11 +50,18 @@ public static class Factories /// /// Resets all factories to their default values. /// + [MemberNotNull( + nameof(CheckFileExists), + nameof(AdbSocketFactory), + nameof(AdbClientFactory), + nameof(AdbCommandLineClientFactory), + nameof(SyncServiceFactory))] public static void Reset() { - AdbSocketFactory = (endPoint) => new AdbSocket(endPoint); - AdbClientFactory = (endPoint) => new AdbClient(endPoint, AdbSocketFactory); - AdbCommandLineClientFactory = (path) => new AdbCommandLineClient(path); + CheckFileExists = File.Exists; + AdbSocketFactory = endPoint => new AdbSocket(endPoint); + AdbClientFactory = endPoint => new AdbClient(endPoint, AdbSocketFactory); + AdbCommandLineClientFactory = path => new AdbCommandLineClient(path); SyncServiceFactory = (client, device) => new SyncService(client, device); } } diff --git a/AdvancedSharpAdbClient/Extensions/LoggerExtensions.cs b/AdvancedSharpAdbClient/Extensions/LoggerExtensions.cs index 89af0cbd..14566e73 100644 --- a/AdvancedSharpAdbClient/Extensions/LoggerExtensions.cs +++ b/AdvancedSharpAdbClient/Extensions/LoggerExtensions.cs @@ -1,19 +1,16 @@ -#if HAS_OLDLOGGER -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// -using Microsoft.Extensions.Logging.Internal; using System; -namespace Microsoft.Extensions.Logging +namespace AdvancedSharpAdbClient { /// /// ILogger extension methods for common scenarios. /// - public static class LoggerExtensionsEx + public static class LoggerExtensions { - private static readonly Func _messageFormatter = MessageFormatter; - //------------------------------------------DEBUG------------------------------------------// /// @@ -24,9 +21,19 @@ public static class LoggerExtensionsEx /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogDebug(exception, "Error while processing request from {Address}", address) - public static void LogDebug(this ILogger logger, Exception exception, string message, params object[] args) => + public static void LogDebug(this ILogger logger, Exception? exception, string? message, params object?[] args) => logger.Log(LogLevel.Debug, exception, message, args); + /// + /// Formats and writes a debug log message. + /// + /// The to write to. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogDebug("Processing request from {Address}", address) + public static void LogDebug(this ILogger logger, string? message, params object?[] args) => + logger.Log(LogLevel.Debug, message, args); + //------------------------------------------TRACE------------------------------------------// /// @@ -37,9 +44,19 @@ public static void LogDebug(this ILogger logger, Exception exception, string mes /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogTrace(exception, "Error while processing request from {Address}", address) - public static void LogTrace(this ILogger logger, Exception exception, string message, params object[] args) => + public static void LogTrace(this ILogger logger, Exception? exception, string? message, params object?[] args) => logger.Log(LogLevel.Trace, exception, message, args); + /// + /// Formats and writes a trace log message. + /// + /// The to write to. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogTrace("Processing request from {Address}", address) + public static void LogTrace(this ILogger logger, string? message, params object?[] args) => + logger.Log(LogLevel.Trace, message, args); + //------------------------------------------INFORMATION------------------------------------------// /// @@ -50,9 +67,19 @@ public static void LogTrace(this ILogger logger, Exception exception, string mes /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogInformation(exception, "Error while processing request from {Address}", address) - public static void LogInformation(this ILogger logger, Exception exception, string message, params object[] args) => + public static void LogInformation(this ILogger logger, Exception? exception, string? message, params object?[] args) => logger.Log(LogLevel.Information, exception, message, args); + /// + /// Formats and writes an informational log message. + /// + /// The to write to. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" + /// An object array that contains zero or more objects to format. + /// logger.LogInformation("Processing request from {Address}", address) + public static void LogInformation(this ILogger logger, string? message, params object?[] args) => + logger.Log(LogLevel.Information, message, args); + //------------------------------------------WARNING------------------------------------------// /// @@ -63,86 +90,73 @@ public static void LogInformation(this ILogger logger, Exception exception, stri /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. /// logger.LogWarning(exception, "Error while processing request from {Address}", address) - public static void LogWarning(this ILogger logger, Exception exception, string message, params object[] args) => + public static void LogWarning(this ILogger logger, Exception? exception, string? message, params object?[] args) => logger.Log(LogLevel.Warning, exception, message, args); - //------------------------------------------ERROR------------------------------------------// - /// - /// Formats and writes an error log message. + /// Formats and writes a warning log message. /// /// The to write to. - /// The exception to log. /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. - /// logger.LogError(exception, "Error while processing request from {Address}", address) - public static void LogError(this ILogger logger, Exception exception, string message, params object[] args) => - logger.Log(LogLevel.Error, exception, message, args); + /// logger.LogWarning("Processing request from {Address}", address) + public static void LogWarning(this ILogger logger, string? message, params object?[] args) => + logger.Log(LogLevel.Warning, message, args); - //------------------------------------------CRITICAL------------------------------------------// + //------------------------------------------ERROR------------------------------------------// /// - /// Formats and writes a critical log message. + /// Formats and writes an error log message. /// /// The to write to. /// The exception to log. /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. - /// logger.LogCritical(exception, "Error while processing request from {Address}", address) - public static void LogCritical(this ILogger logger, Exception exception, string message, params object[] args) => - logger.Log(LogLevel.Critical, exception, message, args); + /// logger.LogError(exception, "Error while processing request from {Address}", address) + public static void LogError(this ILogger logger, Exception? exception, string? message, params object?[] args) => + logger.Log(LogLevel.Error, exception, message, args); /// - /// Formats and writes a log message at the specified log level. + /// Formats and writes an error log message. /// /// The to write to. - /// Entry will be written on this level. - /// Format string of the log message. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. - public static void Log(this ILogger logger, LogLevel logLevel, string message, params object[] args) => - logger.Log(logLevel, 0, null, message, args); + /// logger.LogError("Processing request from {Address}", address) + public static void LogError(this ILogger logger, string? message, params object?[] args) => + logger.Log(LogLevel.Error, message, args); + + //------------------------------------------CRITICAL------------------------------------------// /// - /// Formats and writes a log message at the specified log level. + /// Formats and writes a critical log message. /// /// The to write to. - /// Entry will be written on this level. - /// The event id associated with the log. - /// Format string of the log message. + /// The exception to log. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. - public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, string message, params object[] args) => - logger.Log(logLevel, eventId, null, message, args); + /// logger.LogCritical(exception, "Error while processing request from {Address}", address) + public static void LogCritical(this ILogger logger, Exception? exception, string? message, params object?[] args) => + logger.Log(LogLevel.Critical, exception, message, args); /// - /// Formats and writes a log message at the specified log level. + /// Formats and writes a critical log message. /// /// The to write to. - /// Entry will be written on this level. - /// The exception to log. - /// Format string of the log message. + /// Format string of the log message in message template format. Example: "User {User} logged in from {Address}" /// An object array that contains zero or more objects to format. - public static void Log(this ILogger logger, LogLevel logLevel, Exception exception, string message, params object[] args) => - logger.Log(logLevel, 0, exception, message, args); + /// logger.LogCritical("Processing request from {Address}", address) + public static void LogCritical(this ILogger logger, string? message, params object?[] args) => + logger.Log(LogLevel.Critical, message, args); /// /// Formats and writes a log message at the specified log level. /// /// The to write to. /// Entry will be written on this level. - /// The event id associated with the log. - /// The exception to log. /// Format string of the log message. /// An object array that contains zero or more objects to format. - public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, string message, params object[] args) - { - if (logger == null) { throw new ArgumentNullException(nameof(logger)); } - - logger.Log(logLevel, eventId, new FormattedLogValues(message, args), exception, _messageFormatter); - } - - //------------------------------------------HELPERS------------------------------------------// - - private static string MessageFormatter(FormattedLogValues state, Exception error) => state.ToString(); + public static void Log(this ILogger logger, LogLevel logLevel, string? message, params object?[] args) => + logger.Log(logLevel, null, message, args); } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/SocketExtensions.cs b/AdvancedSharpAdbClient/Extensions/SocketExtensions.cs deleted file mode 100644 index d7c19c03..00000000 --- a/AdvancedSharpAdbClient/Extensions/SocketExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -#if HAS_TASK -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using System; -using System.Net.Sockets; -using System.Threading; - -namespace AdvancedSharpAdbClient -{ - /// - /// Provides extension methods for the class. - /// - internal static class SocketExtensions - { - /// - /// Asynchronously receives data from a connected socket. - /// - /// The socket from which to read data. - /// An array of type that is the storage location for the received data. - /// The zero-based position in the parameter at which to start storing data. - /// The number of bytes to receive. - /// A bitwise combination of the values. - /// A which can be used to cancel the asynchronous task. - /// Cancelling the task will also close the socket. - /// The number of bytes received. - public static Task ReceiveAsync( - this Socket socket, - byte[] buffer, - int offset, - int size, - SocketFlags socketFlags, - CancellationToken cancellationToken) - { -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return socket.ReceiveAsync(buffer.AsMemory(offset, size), socketFlags, cancellationToken).AsTask(); -#elif HAS_PROCESS - // Register a callback so that when a cancellation is requested, the socket is closed. - // This will cause an ObjectDisposedException to bubble up via TrySetResult, which we can catch - // and convert to a TaskCancelledException - which is the exception we expect. - CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(socket.Dispose); - - TaskCompletionSource taskCompletionSource = new(socket); - - socket.BeginReceive(buffer, offset, size, socketFlags, (iar) => - { - // this is the callback - - TaskCompletionSource taskCompletionSource2 = (TaskCompletionSource)iar.AsyncState; - Socket socket2 = (Socket)taskCompletionSource2.Task.AsyncState; - - try - { - taskCompletionSource2.TrySetResult(socket2.EndReceive(iar)); - } - catch (Exception ex) - { - if (ex is ObjectDisposedException && cancellationToken.IsCancellationRequested) - { - taskCompletionSource2.TrySetCanceled(); - } - else - { - taskCompletionSource2.TrySetException(ex); - } - } - finally - { - cancellationTokenRegistration.Dispose(); - } - }, taskCompletionSource); - - return taskCompletionSource.Task; -#else - return Utilities.Run(() => socket.Receive(buffer, offset, size, socketFlags), cancellationToken); -#endif - } - } -} -#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/SyncCommandConverter.cs b/AdvancedSharpAdbClient/Extensions/SyncCommandConverter.cs index 8c9490eb..e265e090 100644 --- a/AdvancedSharpAdbClient/Extensions/SyncCommandConverter.cs +++ b/AdvancedSharpAdbClient/Extensions/SyncCommandConverter.cs @@ -2,10 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; -using System.Collections.Generic; -using System.Linq; namespace AdvancedSharpAdbClient { @@ -14,22 +11,6 @@ namespace AdvancedSharpAdbClient /// public static class SyncCommandConverter { - /// - /// Maps the values to their string representations. - /// - private static readonly Dictionary Values = new() - { - { SyncCommand.DATA, "DATA" }, - { SyncCommand.DENT, "DENT" }, - { SyncCommand.DONE, "DONE" }, - { SyncCommand.FAIL, "FAIL" }, - { SyncCommand.LIST, "LIST" }, - { SyncCommand.OKAY, "OKAY" }, - { SyncCommand.RECV, "RECV" }, - { SyncCommand.SEND, "SEND" }, - { SyncCommand.STAT, "STAT" } - }; - /// /// Gets the byte array that represents the . /// @@ -37,12 +18,25 @@ public static class SyncCommandConverter /// A byte array that represents the . public static byte[] GetBytes(SyncCommand command) { - if (!Values.ContainsKey(command)) + if (command == 0) + { + return [0, 0, 0, 0]; + } + + if (command is not (SyncCommand.LIST + or SyncCommand.RECV + or SyncCommand.SEND + or SyncCommand.STAT + or SyncCommand.DENT + or SyncCommand.FAIL + or SyncCommand.DATA + or SyncCommand.OKAY + or SyncCommand.DONE)) { throw new ArgumentOutOfRangeException(nameof(command), $"{command} is not a valid sync command"); } - string commandText = Values[command]; + string commandText = command.ToString(); byte[] commandBytes = AdbClient.Encoding.GetBytes(commandText); return commandBytes; @@ -53,9 +47,15 @@ public static byte[] GetBytes(SyncCommand command) /// /// A byte array that represents a . /// The corresponding . +#if HAS_BUFFERS + public static SyncCommand GetCommand(ReadOnlySpan value) +#else public static SyncCommand GetCommand(byte[] value) +#endif { +#if !HAS_BUFFERS ExceptionExtensions.ThrowIfNull(value); +#endif if (value.Length != 4) { @@ -63,10 +63,7 @@ public static SyncCommand GetCommand(byte[] value) } string commandText = AdbClient.Encoding.GetString(value); - - SyncCommand? key = Values.Where(d => string.Equals(d.Value, commandText, StringComparison.OrdinalIgnoreCase)).Select(d => new SyncCommand?(d.Key)).SingleOrDefault(); - - return key == null ? throw new ArgumentOutOfRangeException(nameof(value), $"{commandText} is not a valid sync command") : key.Value; + return commandText == "\0\0\0\0" ? 0 : EnumExtensions.TryParse(commandText, true, out SyncCommand command) ? command : throw new ArgumentOutOfRangeException(nameof(value), $"{commandText} is not a valid sync command"); } } } diff --git a/AdvancedSharpAdbClient/Extensions/TaskToApm.cs b/AdvancedSharpAdbClient/Extensions/TaskToApm.cs deleted file mode 100644 index a0b7a9d3..00000000 --- a/AdvancedSharpAdbClient/Extensions/TaskToApm.cs +++ /dev/null @@ -1,123 +0,0 @@ -#if !HAS_PROCESS -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Helper methods for using Tasks to implement the APM pattern. -// -// Example usage, wrapping a Task-returning FooAsync method with Begin/EndFoo methods: -// -// public IAsyncResult BeginFoo(..., AsyncCallback? callback, object? state) => -// TaskToApm.Begin(FooAsync(...), callback, state); -// -// public int EndFoo(IAsyncResult asyncResult) => -// TaskToApm.End(asyncResult); - -namespace System.Threading.Tasks -{ - /// - /// Provides support for efficiently using Tasks to implement the APM (Begin/End) pattern. - /// - internal static class TaskToApm - { - /// - /// Marshals the Task as an IAsyncResult, using the supplied callback and state - /// to implement the APM pattern. - /// - /// The Task to be marshaled. - /// The callback to be invoked upon completion. - /// The state to be stored in the IAsyncResult. - /// An IAsyncResult to represent the task's asynchronous operation. - public static IAsyncResult Begin(Task task, AsyncCallback callback, object state) => - new TaskAsyncResult(task, state, callback); - - /// Processes an IAsyncResult returned by Begin. - /// The IAsyncResult to unwrap. - public static void End(IAsyncResult asyncResult) - { - if (GetTask(asyncResult) is Task t) - { - t.GetAwaiter().GetResult(); - return; - } - - ThrowArgumentException(asyncResult); - } - - /// Processes an IAsyncResult returned by Begin. - /// The IAsyncResult to unwrap. - public static TResult End(IAsyncResult asyncResult) - { - if (GetTask(asyncResult) is Task task) - { - return task.GetAwaiter().GetResult(); - } - - ThrowArgumentException(asyncResult); - return default!; // unreachable - } - - /// Gets the task represented by the IAsyncResult. - public static Task GetTask(IAsyncResult asyncResult) => (asyncResult as TaskAsyncResult)?._task; - - /// Throws an argument exception for the invalid . - private static void ThrowArgumentException(IAsyncResult asyncResult) => - throw (asyncResult is null - ? new ArgumentNullException(nameof(asyncResult)) - : new ArgumentException(null, nameof(asyncResult))); - - /// Provides a simple IAsyncResult that wraps a Task. - /// - /// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state, - /// but that's very rare, in particular in a situation where someone cares about allocation, and always - /// using TaskAsyncResult simplifies things and enables additional optimizations. - /// - internal sealed class TaskAsyncResult : IAsyncResult - { - /// The wrapped Task. - internal readonly Task _task; - /// Callback to invoke when the wrapped task completes. - private readonly AsyncCallback _callback; - - /// Initializes the IAsyncResult with the Task to wrap and the associated object state. - /// The Task to wrap. - /// The new AsyncState value. - /// Callback to invoke when the wrapped task completes. - internal TaskAsyncResult(Task task, object state, AsyncCallback callback) - { - _task = task; - AsyncState = state; - - if (task.IsCompleted) - { - // Synchronous completion. Invoke the callback. No need to store it. - CompletedSynchronously = true; - callback?.Invoke(this); - } - else if (callback != null) - { - // Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in - // order to avoid running synchronously if the task has already completed by the time we get here but still run - // synchronously as part of the task's completion if the task completes after (the more common case). - _callback = callback; - _task.ConfigureAwait(continueOnCapturedContext: false) - .GetAwaiter() - .OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure - } - } - - /// Invokes the callback. - private void InvokeCallback() => _callback.Invoke(this); - - /// Gets a user-defined object that qualifies or contains information about an asynchronous operation. - public object AsyncState { get; } - /// Gets a value that indicates whether the asynchronous operation completed synchronously. - /// This is set lazily based on whether the has completed by the time this object is created. - public bool CompletedSynchronously { get; } - /// Gets a value that indicates whether the asynchronous operation has completed. - public bool IsCompleted => _task.IsCompleted; - /// Gets a that is used to wait for an asynchronous operation to complete. - public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; - } - } -} -#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/Utilities.cs b/AdvancedSharpAdbClient/Extensions/Utilities.cs deleted file mode 100644 index 2d5cfb5f..00000000 --- a/AdvancedSharpAdbClient/Extensions/Utilities.cs +++ /dev/null @@ -1,327 +0,0 @@ -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using AdvancedSharpAdbClient.Exceptions; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace AdvancedSharpAdbClient -{ - internal static class Utilities - { - /// - /// Converts the string representation of the name or numeric value of one or more - /// enumerated constants to an equivalent enumerated object. A parameter specifies - /// whether the operation is case-sensitive. The return value indicates whether the - /// conversion succeeded. - /// - /// The enumeration type to which to convert . - /// The string representation of the enumeration name or underlying value to convert. - /// to ignore case; to consider case. - /// When this method returns, contains an object of type whose - /// value is represented by if the parse operation succeeds. If the parse operation fails, - /// contains the default value of the underlying type of . This parameter is passed uninitialized. - /// if the value parameter was converted successfully; otherwise, . - /// is not an enumeration type. - public static bool TryParse(string value, bool ignoreCase, out TEnum result) where TEnum : struct - { -#if NETFRAMEWORK && !NET40_OR_GREATER - string strTypeFixed = value.Replace(' ', '_'); - if (Enum.IsDefined(typeof(TEnum), strTypeFixed)) - { - result = (TEnum)Enum.Parse(typeof(TEnum), strTypeFixed, ignoreCase); - return true; - } - else - { - foreach (string str in Enum.GetNames(typeof(TEnum))) - { - if (str.Equals(strTypeFixed, StringComparison.OrdinalIgnoreCase)) - { - result = (TEnum)Enum.Parse(typeof(TEnum), str, ignoreCase); - return true; - } - } - result = default; - return false; - } -#else - return Enum.TryParse(value, ignoreCase, out result); -#endif - } - - /// - /// Indicates whether a specified string is , empty, or consists only of white-space characters. - /// - /// The string to test. - /// if the parameter is or - /// , or if consists exclusively of white-space characters. - public static bool IsNullOrWhiteSpace(this string value) - { -#if NETFRAMEWORK && !NET40_OR_GREATER - if (value == null) - { - return true; - } - - for (int i = 0; i < value.Length; i++) - { - if (!char.IsWhiteSpace(value[i])) - { - return false; - } - } - - return true; -#else - return string.IsNullOrWhiteSpace(value); -#endif - } - - /// - /// Concatenates the members of a constructed collection of type , - /// using the specified separator between each member. - /// - /// The string to use as a separator. is included - /// in the returned string only if has more than one element. - /// A collection that contains the strings to concatenate. - /// A string that consists of the elements of delimited by the - /// string.-or- if values has zero elements. - public static string Join(string separator, IEnumerable values) - { -#if NETFRAMEWORK && !NET40_OR_GREATER - ExceptionExtensions.ThrowIfNull(values); - - separator ??= string.Empty; - - using IEnumerator en = values.GetEnumerator(); - if (!en.MoveNext()) - { - return string.Empty; - } - - StringBuilder result = new(); - if (en.Current != null) - { - _ = result.Append(en.Current); - } - - while (en.MoveNext()) - { - _ = result.Append(separator); - if (en.Current != null) - { - _ = result.Append(en.Current); - } - } - return result.ToString(); -#else - return string.Join(separator, values); -#endif - } - -#if NETFRAMEWORK && !NET40_OR_GREATER - /// - /// Removes all characters from the current instance. - /// - /// The to removes all characters. - /// An object whose is 0 (zero). - public static StringBuilder Clear(this StringBuilder builder) - { - builder.Length = 0; - return builder; - } - - /// - /// Releases all resources used by the current instance of the class. - /// - /// The to release. - public static void Dispose(this Socket socket) - { - socket.Close(); - GC.SuppressFinalize(socket); - } - - /// - /// Releases all resources used by the current instance of the class. - /// - /// The to release. - public static void Dispose(this WaitHandle waitHandle) - { - waitHandle.Close(); - GC.SuppressFinalize(waitHandle); - } -#endif - -#if HAS_TASK - /// - /// Creates a task that completes after a specified number of milliseconds. - /// - /// The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. - /// A cancellation token to observe while waiting for the task to complete. - /// A task that represents the time delay. - /// The argument is less than -1. - public static Task Delay(int dueTime, CancellationToken cancellationToken = default) => -#if NETFRAMEWORK && !NET45_OR_GREATER - TaskEx -#else - Task -#endif - .Delay(dueTime, cancellationToken); - - /// - /// Queues the specified work to run on the thread pool and returns a proxy for the task returned by . - /// - /// The work to execute asynchronously. - /// A cancellation token that can be used to cancel the work if it has not yet started. - /// A task that represents a proxy for the task returned by . - /// The parameter was . - /// For information on handling exceptions thrown by task operations, see Exception Handling. - public static Task Run(Action function, CancellationToken cancellationToken = default) => -#if NETFRAMEWORK && !NET45_OR_GREATER - TaskEx -#else - Task -#endif - .Run(function, cancellationToken); - - /// - /// Queues the specified work to run on the thread pool and returns a proxy for the - /// returned by function. A cancellation token allows the work to be cancelled if it has not yet started. - /// - /// The type of the result returned by the proxy task. - /// The work to execute asynchronously. - /// A cancellation token that can be used to cancel the work if it has not yet started. - /// A that represents a proxy for the - /// returned by . - /// The parameter was . - public static Task Run(Func function, CancellationToken cancellationToken = default) => -#if NETFRAMEWORK && !NET45_OR_GREATER - TaskEx -#else - Task -#endif - .Run(function, cancellationToken); - -#if !NET7_0_OR_GREATER - /// - /// Reads a line of characters asynchronously and returns the data as a string. - /// - /// The to read a line. - /// The token to monitor for cancellation requests. - /// A value task that represents the asynchronous read operation. The value of the - /// TResult parameter contains the next line from the text reader, or is null if - /// all of the characters have been read. - public static -#if NET7_0_OR_GREATER - ValueTask -#else - Task -#endif - ReadLineAsync(this TextReader reader, CancellationToken cancellationToken) => -#if !NET35 - reader.ReadLineAsync( -#if NET7_0_OR_GREATER - cancellationToken -#endif - ); -#else - Run(reader.ReadLine, cancellationToken); -#endif - - /// - /// Reads all characters from the current position to the end of the stream asynchronously and returns them as one string. - /// - /// The to read all characters. - /// The token to monitor for cancellation requests. - /// A task that represents the asynchronous read operation. The value of the TResult - /// parameter contains a string with the characters from the current position to - /// the end of the stream. - public static Task ReadToEndAsync(this TextReader reader, CancellationToken cancellationToken) => -#if !NET35 - reader.ReadToEndAsync( -#if NET7_0_OR_GREATER - cancellationToken -#endif - ); -#else - Run(reader.ReadToEnd, cancellationToken); -#endif -#endif -#endif - -#if NET20 -#pragma warning disable CS1574 // XML 注释中有无法解析的 cref 特性 -#endif - /// - /// Converts a Unix time expressed as the number of seconds that have elapsed - /// since 1970-01-01T00:00:00Z to a value. - /// - /// A Unix time, expressed as the number of seconds that have elapsed - /// since 1970-01-01T00:00:00Z (January 1, 1970, at 12:00 AM UTC). For Unix times before this date, - /// its value is negative. - /// A date and time value that represents the same moment in time as the Unix time. - /// is less than -62,135,596,800. - /// -or- is greater than 253,402,300,799. - /// The Offset property value of the returned instance is - /// , which represents Coordinated Universal Time. You can convert it to the time in - /// a specific time zone by calling the method. - public static DateTimeOffset FromUnixTimeSeconds(long seconds) => -#if NETFRAMEWORK && !NET46_OR_GREATER - new(seconds.ToDateTime()); -#else - DateTimeOffset.FromUnixTimeSeconds(seconds); -#endif -#if NET20 -#pragma warning restore CS1574 // XML 注释中有无法解析的 cref 特性 -#endif - - /// - /// Returns the number of seconds that have elapsed since 1970-01-01T00:00:00Z. - /// - /// The DateTimeOffset - /// The number of seconds that have elapsed since 1970-01-01T00:00:00Z. - public static long ToUnixTimeSeconds(this DateTimeOffset dateTimeOffset) => -#if NETFRAMEWORK && !NET46_OR_GREATER - dateTimeOffset.DateTime.ToUnixEpoch(); -#else - dateTimeOffset.ToUnixTimeSeconds(); -#endif - - public static bool IsWindowsPlatform() => -#if HAS_RUNTIMEINFORMATION - RuntimeInformation.IsOSPlatform(OSPlatform.Windows); -#elif NETCORE - true; -#else - Environment.OSVersion.Platform - is PlatformID.Win32S - or PlatformID.Win32Windows - or PlatformID.Win32NT - or PlatformID.WinCE - or PlatformID.Xbox; -#endif - - public static bool IsUnixPlatform() => -#if HAS_RUNTIMEINFORMATION - RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - || RuntimeInformation.IsOSPlatform(OSPlatform.OSX) -#if NETCOREAPP3_0_OR_GREATER - || RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) -#endif - ; -#elif NETCORE - false; -#else - Environment.OSVersion.Platform - is PlatformID.Unix - or PlatformID.MacOSX; -#endif - } -} diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbClient.Async.cs b/AdvancedSharpAdbClient/Interfaces/IAdbClient.Async.cs index 886022de..dc7cb43f 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbClient.Async.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbClient.Async.cs @@ -3,8 +3,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; using System; using System.Collections.Generic; using System.IO; @@ -78,44 +76,6 @@ public partial interface IAdbClient /// which has been opened. In all other cases, 0. Task CreateForwardAsync(DeviceData device, string local, string remote, bool allowRebind, CancellationToken cancellationToken); - /// - /// Asks the ADB server to forward local connections from - /// to the address on the . - /// - /// The device on which to forward the connections. - /// - /// The local address to forward. This value can be in one of: - /// - /// - /// tcp:<port>: TCP connection on localhost:<port> - /// - /// - /// local:<path>: Unix local domain socket on <path> - /// - /// - /// - /// - /// The remote address to forward. This value can be in one of: - /// - /// - /// tcp:<port>: TCP connection on localhost:<port> on device - /// - /// - /// local:<path>: Unix local domain socket on <path> on device - /// - /// - /// jdwp:<pid>: JDWP thread on VM process <pid> on device. - /// - /// - /// - /// If set to , the request will fail if there is already a forward - /// connection from . - /// A which can be used to cancel the asynchronous operation. - /// A which represents the asynchronous operation. - /// If your requested to start forwarding to local port TCP:0, the port number of the TCP port - /// which has been opened. In all other cases, 0. - Task CreateForwardAsync(DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind, CancellationToken cancellationToken); - /// /// Asks the ADB server to reverse forward local connections from /// to the address on the . @@ -204,22 +164,70 @@ public partial interface IAdbClient /// A which return the entry for each existing reverse forward connection. Task> ListReverseForwardAsync(DeviceData device, CancellationToken cancellationToken); + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The encoding to use when parsing the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + Task ExecuteServerCommandAsync(string target, string command, Encoding encoding, CancellationToken cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// The encoding to use when parsing the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + Task ExecuteServerCommandAsync(string target, string command, IAdbSocket socket, Encoding encoding, CancellationToken cancellationToken); + /// /// Executes a command on the device. /// /// The command to execute. /// The device on which to run the command. - /// The receiver which will get the command output. + /// The encoding to use when parsing the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + Task ExecuteRemoteCommandAsync(string command, DeviceData device, Encoding encoding, CancellationToken cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// Optionally, a that processes the command output. + /// The encoding to use when parsing the command output. /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. - Task ExecuteRemoteCommandAsync(string command, DeviceData device, IShellOutputReceiver receiver, CancellationToken cancellationToken); + Task ExecuteServerCommandAsync(string target, string command, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken); + + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// Optionally, a that processes the command output. + /// The encoding to use when parsing the command output. + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + Task ExecuteServerCommandAsync(string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, Encoding encoding, CancellationToken cancellationToken); /// /// Executes a command on the device. /// /// The command to execute. /// The device on which to run the command. - /// The receiver which will get the command output. + /// Optionally, a that processes the command output. /// The encoding to use when parsing the command output. /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. @@ -235,15 +243,6 @@ public partial interface IAdbClient /// failed nudging Task GetFrameBufferAsync(DeviceData device, CancellationToken cancellationToken); - /// - /// Asynchronously runs the event log service on a device. - /// - /// The device on which to run the event log service. - /// A callback which will receive the event log messages as they are received. - /// Optionally, the names of the logs to receive. - /// A which represents the asynchronous operation. - Task RunLogServiceAsync(DeviceData device, Action messageSink, params LogId[] logNames); - /// /// Asynchronously runs the event log service on a device. /// @@ -304,15 +303,6 @@ public partial interface IAdbClient /// A which represents the asynchronous operation. Task UnrootAsync(DeviceData device, CancellationToken cancellationToken); - /// - /// Asynchronously installs an Android application on an device. - /// - /// The device on which to install the application. - /// A which represents the application to install. - /// The arguments to pass to adb install. - /// A which represents the asynchronous operation. - Task InstallAsync(DeviceData device, Stream apk, params string[] arguments); - /// /// Asynchronously installs an Android application on an device. /// @@ -323,16 +313,6 @@ public partial interface IAdbClient /// A which represents the asynchronous operation. Task InstallAsync(DeviceData device, Stream apk, CancellationToken cancellationToken, params string[] arguments); - /// - /// Asynchronously push multiple APKs to the device and install them. - /// - /// The device on which to install the application. - /// s which represents the split APKs to install. - /// The package name of the base APK to install. - /// The arguments to pass to adb install-create. - /// A which represents the asynchronous operation. - Task InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, params string[] arguments); - /// /// Asynchronously push multiple APKs to the device and install them. /// @@ -344,16 +324,6 @@ public partial interface IAdbClient /// A which represents the asynchronous operation. Task InstallMultipleAsync(DeviceData device, IEnumerable splitAPKs, string packageName, CancellationToken cancellationToken, params string[] arguments); - /// - /// Asynchronously push multiple APKs to the device and install them. - /// - /// The device on which to install the application. - /// A which represents the base APK to install. - /// s which represents the split APKs to install. - /// The arguments to pass to adb install-create. - /// A which represents the asynchronous operation. - Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, params string[] arguments); - /// /// Asynchronously push multiple APKs to the device and install them. /// @@ -365,15 +335,6 @@ public partial interface IAdbClient /// A which represents the asynchronous operation. Task InstallMultipleAsync(DeviceData device, Stream baseAPK, IEnumerable splitAPKs, CancellationToken cancellationToken, params string[] arguments); - /// - /// Like "install", but starts an install session. - /// - /// The device on which to install the application. - /// The package name of the baseAPK to install. - /// The arguments to pass to adb install-create. - /// A which return the session ID - Task InstallCreateAsync(DeviceData device, string packageName, params string[] arguments); - /// /// Like "install", but starts an install session. /// @@ -382,7 +343,7 @@ public partial interface IAdbClient /// A which can be used to cancel the asynchronous operation. /// The arguments to pass to adb install-create. /// A which return the session ID - Task InstallCreateAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments); + Task InstallCreateAsync(DeviceData device, string? packageName, CancellationToken cancellationToken, params string[] arguments); /// /// Write an apk into the given install session. @@ -404,6 +365,16 @@ public partial interface IAdbClient /// A which represents the asynchronous operation. Task InstallCommitAsync(DeviceData device, string session, CancellationToken cancellationToken); + /// + /// Uninstalls an Android application on an device. + /// + /// The device on which to install the application. + /// The name of the package to uninstall. + /// A which can be used to cancel the asynchronous operation. + /// The arguments to pass to adb uninstall. + /// A which represents the asynchronous operation. + Task UninstallAsync(DeviceData device, string packageName, CancellationToken cancellationToken, params string[] arguments); + /// /// Lists all features supported by the current device. /// @@ -427,7 +398,7 @@ public partial interface IAdbClient /// The device for which to get the screen snapshot. /// A which can be used to cancel the asynchronous operation. /// A which return a containing current hierarchy. - Task DumpScreenAsync(DeviceData device, CancellationToken cancellationToken); + Task DumpScreenAsync(DeviceData device, CancellationToken cancellationToken); #if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER /// @@ -436,17 +407,17 @@ public partial interface IAdbClient /// The device for which to get the screen snapshot. /// A which can be used to cancel the asynchronous operation. /// A which return a containing current hierarchy. - Task DumpScreenWinRTAsync(DeviceData device, CancellationToken cancellationToken); + Task DumpScreenWinRTAsync(DeviceData device, CancellationToken cancellationToken); #endif /// /// Clicks on the specified coordinates. /// /// The device on which to click. - /// The to click. + /// The to click. /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. - Task ClickAsync(DeviceData device, Cords cords, CancellationToken cancellationToken); + Task ClickAsync(DeviceData device, Point cords, CancellationToken cancellationToken); /// /// Clicks on the specified coordinates. @@ -489,7 +460,7 @@ public partial interface IAdbClient /// The package name of the app to check. /// A which can be used to cancel the asynchronous operation. /// A which return the result. if the app is running in foreground; otherwise, . - Task IsCurrentAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken); + Task IsAppInForegroundAsync(DeviceData device, string packageName, CancellationToken cancellationToken); /// /// Check if the app is running in background. @@ -517,7 +488,7 @@ public partial interface IAdbClient /// A which can be used to cancel the asynchronous operation. /// Only check once if . Or it will continue check until is . /// A which return the of . - Task FindElementAsync(DeviceData device, string xpath, CancellationToken cancellationToken); + Task FindElementAsync(DeviceData device, string xpath, CancellationToken cancellationToken); /// /// Get elements by xpath asynchronously. You can specify the waiting time in timeout. @@ -527,7 +498,7 @@ public partial interface IAdbClient /// A which can be used to cancel the asynchronous operation. /// Only check once if . Or it will continue check until is . /// A which return the of has got. - Task> FindElementsAsync(DeviceData device, string xpath, CancellationToken cancellationToken); + Task> FindElementsAsync(DeviceData device, string xpath, CancellationToken cancellationToken); #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER /// @@ -559,15 +530,6 @@ public partial interface IAdbClient /// A which represents the asynchronous operation. Task SendTextAsync(DeviceData device, string text, CancellationToken cancellationToken); - /// - /// Clear the input text. The input should be in focus. Use if the element isn't focused. - /// - /// The device on which to clear the input text. - /// The length of text to clear. - /// A which can be used to cancel the asynchronous operation. - /// A which represents the asynchronous operation. - Task ClearInputAsync(DeviceData device, int charCount, CancellationToken cancellationToken); - /// /// Start an Android application on device. /// @@ -585,20 +547,6 @@ public partial interface IAdbClient /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. Task StopAppAsync(DeviceData device, string packageName, CancellationToken cancellationToken); - - /// - /// Click BACK button. - /// - /// The device on which to click BACK button. - /// A which represents the asynchronous operation. - Task BackBtnAsync(DeviceData device); - - /// - /// Click HOME button. - /// - /// The device on which to click HOME button. - /// A which represents the asynchronous operation. - Task HomeBtnAsync(DeviceData device); } } #endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbClient.cs b/AdvancedSharpAdbClient/Interfaces/IAdbClient.cs index 7aa8927a..b297fd4d 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbClient.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbClient.cs @@ -2,10 +2,9 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; -using AdvancedSharpAdbClient.Logs; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Net; using System.Text; @@ -105,42 +104,6 @@ public partial interface IAdbClient /// which has been opened. In all other cases, 0. int CreateForward(DeviceData device, string local, string remote, bool allowRebind); - /// - /// Asks the ADB server to forward local connections from - /// to the address on the . - /// - /// The device on which to forward the connections. - /// - /// The local address to forward. This value can be in one of: - /// - /// - /// tcp:<port>: TCP connection on localhost:<port> - /// - /// - /// local:<path>: Unix local domain socket on <path> - /// - /// - /// - /// - /// The remote address to forward. This value can be in one of: - /// - /// - /// tcp:<port>: TCP connection on localhost:<port> on device - /// - /// - /// local:<path>: Unix local domain socket on <path> on device - /// - /// - /// jdwp:<pid>: JDWP thread on VM process <pid> on device. - /// - /// - /// - /// If set to , the request will fail if there is already a forward - /// connection from . - /// If your requested to start forwarding to local port TCP:0, the port number of the TCP port - /// which has been opened. In all other cases, 0. - int CreateForward(DeviceData device, ForwardSpec local, ForwardSpec remote, bool allowRebind); - /// /// Asks the ADB server to reverse forward local connections from /// to the address on the . @@ -218,19 +181,59 @@ public partial interface IAdbClient IEnumerable ListReverseForward(DeviceData device); /// - /// Executes a command on the device. + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The encoding to use when parsing the command output. + void ExecuteServerCommand(string target, string command, Encoding encoding); + + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// The encoding to use when parsing the command output. + void ExecuteServerCommand(string target, string command, IAdbSocket socket, Encoding encoding); + + /// + /// Executes a shell command on the device. /// /// The command to execute. /// The device on which to run the command. - /// The receiver which will get the command output. - void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver); + /// The encoding to use when parsing the command output. + void ExecuteRemoteCommand(string command, DeviceData device, Encoding encoding); + + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// Optionally, a that processes the command output. + /// The encoding to use when parsing the command output. + void ExecuteServerCommand(string target, string command, IShellOutputReceiver receiver, Encoding encoding); + + /// + /// Executes a command on the adb server. + /// + /// The target of command, such as shell, remount, dev, tcp, local, + /// localreserved, localabstract, jdwp, track-jdwp, sync, reverse and so on. + /// The command to execute. + /// The to send command. + /// Optionally, a that processes the command output. + /// The encoding to use when parsing the command output. + void ExecuteServerCommand(string target, string command, IAdbSocket socket, IShellOutputReceiver receiver, Encoding encoding); /// - /// Executes a command on the device. + /// Executes a shell command on the device. /// /// The command to execute. /// The device on which to run the command. - /// The receiver which will get the command output. + /// Optionally, a that processes the command output. /// The encoding to use when parsing the command output. void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding); @@ -347,7 +350,7 @@ public partial interface IAdbClient /// The package name of the baseAPK to install. /// The arguments to pass to adb install-create. /// The session ID of this install session. - string InstallCreate(DeviceData device, string packageName = null, params string[] arguments); + string InstallCreate(DeviceData device, string? packageName = null, params string[] arguments); /// /// Write an apk into the given install session. @@ -365,6 +368,14 @@ public partial interface IAdbClient /// The session ID of the install session. void InstallCommit(DeviceData device, string session); + /// + /// Uninstalls an Android application on an device. + /// + /// The device on which to install the application. + /// The name of the package to uninstall. + /// The arguments to pass to adb uninstall. + void Uninstall(DeviceData device, string packageName, params string[] arguments); + /// /// Lists all features supported by the current device. /// @@ -385,7 +396,7 @@ public partial interface IAdbClient /// /// The device for which to get the screen snapshot. /// A containing current hierarchy. - XmlDocument DumpScreen(DeviceData device); + XmlDocument? DumpScreen(DeviceData device); #if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER /// @@ -393,15 +404,15 @@ public partial interface IAdbClient /// /// The device for which to get the screen snapshot. /// A containing current hierarchy. - Windows.Data.Xml.Dom.XmlDocument DumpScreenWinRT(DeviceData device); + Windows.Data.Xml.Dom.XmlDocument? DumpScreenWinRT(DeviceData device); #endif /// /// Clicks on the specified coordinates. /// /// The device on which to click. - /// The to click. - void Click(DeviceData device, Cords cords); + /// The to click. + void Click(DeviceData device, Point cords); /// /// Clicks on the specified coordinates. @@ -437,7 +448,7 @@ public partial interface IAdbClient /// The device on which to check. /// The package name of the app to check. /// if the app is running in foreground; otherwise, . - bool IsCurrentApp(DeviceData device, string packageName); + bool IsAppInForeground(DeviceData device, string packageName); /// /// Check if the app is running in background. @@ -463,7 +474,7 @@ public partial interface IAdbClient /// The timeout for waiting the element. /// Only check once if or . /// The of . - Element FindElement(DeviceData device, string xpath, TimeSpan timeout = default); + Element? FindElement(DeviceData device, string xpath, TimeSpan timeout = default); /// /// Get elements by xpath. You can specify the waiting time in timeout. @@ -489,13 +500,6 @@ public partial interface IAdbClient /// The text to send. void SendText(DeviceData device, string text); - /// - /// Clear the input text. The input should be in focus. Use if the element isn't focused. - /// - /// The device on which to clear the input text. - /// The length of text to clear. - void ClearInput(DeviceData device, int charCount); - /// /// Start an Android application on device. /// @@ -509,24 +513,13 @@ public partial interface IAdbClient /// The device on which to stop an application. /// The package name of the application to stop. void StopApp(DeviceData device, string packageName); - - /// - /// Click BACK button. - /// - /// The device on which to click BACK button. - void BackBtn(DeviceData device); - - /// - /// Click HOME button. - /// - /// The device on which to click HOME button. - void HomeBtn(DeviceData device); } /// /// See as the interface. /// - [Obsolete("IAdvancedAdbClient is too long to remember. Please use IAdbClient instead.")] + [Obsolete($"{nameof(IAdbCommandLineClient)} is too long to remember. Please use {nameof(IAdbClient)} instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] public interface IAdvancedAdbClient : IAdbClient { } diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbCommandLineClient.cs b/AdvancedSharpAdbClient/Interfaces/IAdbCommandLineClient.cs index a26e30a9..f5eaffc0 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbCommandLineClient.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbCommandLineClient.cs @@ -22,9 +22,9 @@ public partial interface IAdbCommandLineClient void StartServer(); /// - /// Throws an error if the path does not point to a valid instance of adb.exe. + /// Determines whether the adb.exe file exists. /// /// The path to validate. - bool IsValidAdbFile(string adbPath); + bool CheckFileExists(string adbPath); } } diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbServer.Async.cs b/AdvancedSharpAdbClient/Interfaces/IAdbServer.Async.cs index 8e40fa49..49fa3840 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbServer.Async.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbServer.Async.cs @@ -3,7 +3,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System.Threading; namespace AdvancedSharpAdbClient @@ -13,16 +12,12 @@ public partial interface IAdbServer /// /// Starts the adb server if it was not previously running. /// - /// - /// The path to the adb.exe executable that can be used to start the adb server. + /// The path to the adb.exe executable that can be used to start the adb server. /// If this path is not provided, this method will throw an exception if the server - /// is not running or is not up to date. - /// - /// - /// to restart the adb server if the version of the adb.exe + /// is not running or is not up to date. + /// to restart the adb server if the version of the adb.exe /// executable at is newer than the version that is currently - /// running; to keep a previous version of the server running. - /// + /// running; to keep a previous version of the server running. /// A which can be used to cancel the asynchronous operation. /// /// A which return @@ -38,15 +33,17 @@ public partial interface IAdbServer /// flag was set. /// /// - /// /// if the adb server was not running, /// and the server was started. + /// + /// + /// if a + /// operation is already in progress. + /// /// /// - /// - /// The server was not running, or an outdated version of the server was running, - /// and the parameter was not specified. - /// + /// The server was not running, or an outdated version of the server was running, + /// and the parameter was not specified. Task StartServerAsync(string adbPath, bool restartServerIfNewer, CancellationToken cancellationToken); /// @@ -56,32 +53,31 @@ public partial interface IAdbServer /// /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. - /// - /// You can only call this method if you have previously started the adb server via - /// and passed the full path to the adb server. - /// + /// You can only call this method if you have previously started the adb server via + /// and passed the full path to the adb server. Task RestartServerAsync(CancellationToken cancellationToken); /// - /// Restarts the adb server if it suddenly became unavailable. Call this class if, for example, + /// Restarts the adb server with new adb path if it suddenly became unavailable. Call this class if, for example, /// you receive an with the flag /// set to - a clear indicating the ADB server died. /// - /// - /// The path to the adb.exe executable that can be used to start the adb server. + /// The path to the adb.exe executable that can be used to start the adb server. /// If this path is not provided, this method will use the path that was cached by - /// - /// + /// /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. - /// - /// You can only call this method if you have previously started the adb server via - /// and passed the full path to the adb server. - /// + /// You can only call this method if you have previously started the adb server via + /// and passed the full path to the adb server. Task RestartServerAsync(string adbPath, CancellationToken cancellationToken); /// - /// Gets the status of the adb server. + /// Stop the adb server asynchronously. + /// + Task StopServerAsync(CancellationToken cancellationToken); + + /// + /// Gets the status of the adb server asynchronously. /// /// A which can be used to cancel the asynchronous operation. /// A which return a object that describes the status of the adb server. diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbServer.cs b/AdvancedSharpAdbClient/Interfaces/IAdbServer.cs index cbbc3912..3ad593ae 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbServer.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbServer.cs @@ -2,8 +2,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; - namespace AdvancedSharpAdbClient { /// @@ -14,16 +12,12 @@ public partial interface IAdbServer /// /// Starts the adb server if it was not previously running. /// - /// - /// The path to the adb.exe executable that can be used to start the adb server. + /// The path to the adb.exe executable that can be used to start the adb server. /// If this path is not provided, this method will throw an exception if the server - /// is not running or is not up to date. - /// - /// - /// to restart the adb server if the version of the adb.exe + /// is not running or is not up to date. + /// to restart the adb server if the version of the adb.exe /// executable at is newer than the version that is currently - /// running; to keep a previous version of the server running. - /// + /// running; to keep a previous version of the server running. /// /// /// @@ -37,15 +31,17 @@ public partial interface IAdbServer /// flag was set. /// /// - /// /// if the adb server was not running, /// and the server was started. + /// + /// + /// if an + /// operation is already in progress. + /// /// /// - /// - /// The server was not running, or an outdated version of the server was running, - /// and the parameter was not specified. - /// + /// The server was not running, or an outdated version of the server was running, + /// and the parameter was not specified. StartServerResult StartServer(string adbPath, bool restartServerIfNewer); /// @@ -53,16 +49,26 @@ public partial interface IAdbServer /// you receive an with the flag /// set to - a clear indicating the ADB server died. /// - /// - /// The path to the adb.exe executable that can be used to start the adb server. + /// You can only call this method if you have previously started the adb server via + /// and passed the full path to the adb server. + StartServerResult RestartServer(); + + /// + /// Restarts the adb server with new adb path if it suddenly became unavailable. Call this class if, for example, + /// you receive an with the flag + /// set to - a clear indicating the ADB server died. + /// + /// The path to the adb.exe executable that can be used to start the adb server. /// If this path is not provided, this method will use the path that was cached by - /// - /// - /// - /// You can only call this method if you have previously started the adb server via - /// and passed the full path to the adb server. - /// - StartServerResult RestartServer(string adbPath = null); + /// + /// You can only call this method if you have previously started the adb server via + /// and passed the full path to the adb server. + StartServerResult RestartServer(string adbPath); + + /// + /// Stop the adb server. + /// + void StopServer(); /// /// Gets the status of the adb server. diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbSocket.Async.cs b/AdvancedSharpAdbClient/Interfaces/IAdbSocket.Async.cs index b406758e..d2af3b88 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbSocket.Async.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbSocket.Async.cs @@ -3,12 +3,35 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // +using System; +using System.Net; +using System.Net.Sockets; using System.Threading; namespace AdvancedSharpAdbClient { public partial interface IAdbSocket { +#if NET6_0_OR_GREATER + /// + /// Reconnects the to the same endpoint it was initially connected to. + /// Use this when the socket was disconnected by adb and you have restarted adb. + /// + /// Force reconnect whatever the socket is connected or not. + /// A which can be used to cancel the asynchronous task. + /// A that represents the asynchronous operation. + ValueTask ReconnectAsync(bool isForce, CancellationToken cancellationToken); +#else + /// + /// Reconnects the to the same endpoint it was initially connected to. + /// Use this when the socket was disconnected by adb and you have restarted adb. + /// + /// Force reconnect whatever the socket is connected or not. + /// A which can be used to cancel the asynchronous task. + /// A that represents the asynchronous operation. + Task ReconnectAsync(bool isForce, CancellationToken cancellationToken); +#endif + /// /// Sends the specified number of bytes of data to a , /// @@ -65,15 +88,6 @@ public partial interface IAdbSocket /// A that represents the asynchronous operation. Task SendAdbRequestAsync(string request, CancellationToken cancellationToken); - /// - /// Reads a from an instance when - /// the connection is in sync mode. - /// - /// The buffer to store the read data into. - /// A that can be used to cancel the task. - /// A that represents the asynchronous operation. - Task ReadAsync(byte[] data, CancellationToken cancellationToken); - /// /// Receives data from a into a receive buffer. /// @@ -114,6 +128,42 @@ public partial interface IAdbSocket /// A object that represents the response from the Android Debug Bridge. Task ReadAdbResponseAsync(CancellationToken cancellationToken); +#if HAS_BUFFERS + /// + /// Sends the specified number of bytes of data to a , + /// + /// A array that acts as a buffer, containing the data to send. + /// A that can be used to cancel the task. + /// A that represents the asynchronous operation. + public ValueTask SendAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default); + + /// + /// Receives data from a into a receive buffer. + /// + /// An array of type Byte that is the storage location for the received data. + /// A that can be used to cancel the task. + /// Cancelling the task will also close the socket. + /// A that represents the asynchronous operation. The result value of the task contains the number of bytes received. + public ValueTask ReadAsync(Memory data, CancellationToken cancellationToken); +#else + /// + /// Sends the specified number of bytes of data to a , + /// + /// A array that acts as a buffer, containing the data to send. + /// A that can be used to cancel the task. + /// A that represents the asynchronous operation. + Task SendAsync(byte[] data, CancellationToken cancellationToken); + + /// + /// Reads a from an instance when + /// the connection is in sync mode. + /// + /// The buffer to store the read data into. + /// A that can be used to cancel the task. + /// A that represents the asynchronous operation. The result value of the task contains the number of bytes received. + Task ReadAsync(byte[] data, CancellationToken cancellationToken); +#endif + /// /// Ask to switch the connection to the device/emulator identified by /// . After this request, every client request will diff --git a/AdvancedSharpAdbClient/Interfaces/IAdbSocket.cs b/AdvancedSharpAdbClient/Interfaces/IAdbSocket.cs index 72617412..cb631fc6 100644 --- a/AdvancedSharpAdbClient/Interfaces/IAdbSocket.cs +++ b/AdvancedSharpAdbClient/Interfaces/IAdbSocket.cs @@ -2,9 +2,9 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.IO; +using System.Net.Sockets; namespace AdvancedSharpAdbClient { @@ -24,7 +24,8 @@ public partial interface IAdbSocket : IDisposable /// Reconnects the to the same endpoint it was initially connected to. /// Use this when the socket was disconnected by adb and you have restarted adb. /// - void Reconnect(); + /// Force reconnect whatever the socket is connected or not. + void Reconnect(bool isForce); /// /// Sends the specified number of bytes of data to a , @@ -71,13 +72,15 @@ public partial interface IAdbSocket : IDisposable void SendAdbRequest(string request); /// - /// Reads from the socket until the array is filled, or no more data is coming(because - /// the socket closed or the timeout expired). + /// Reads from the socket until the array is filled, the optional + /// is reached, or no more data is coming (because the socket closed or the + /// timeout expired). /// - /// The buffer to store the read data into. + /// The buffer to store the read data into. + /// The length to read or -1 to fill the data buffer completely /// The total number of bytes read. - /// This uses the default time out value. - int Read(byte[] data); + /// EOF or No Data to read: exception.Message + int Read(byte[] data, int length); /// /// Reads from the socket until the array is filled, the optional @@ -85,10 +88,11 @@ public partial interface IAdbSocket : IDisposable /// timeout expired). /// /// The buffer to store the read data into. + /// The position to store the received data. /// The length to read or -1 to fill the data buffer completely /// The total number of bytes read. /// EOF or No Data to read: exception.Message - int Read(byte[] data, int length); + int Read(byte[] data, int offset, int length); /// /// Reads a from an instance. @@ -116,6 +120,39 @@ public partial interface IAdbSocket : IDisposable /// A object that represents the response from the Android Debug Bridge. AdbResponse ReadAdbResponse(); +#if HAS_BUFFERS + /// + /// Sends the specified number of bytes of data to a , + /// + /// A span of bytes that acts as a buffer, containing the data to send. + void Send(ReadOnlySpan data); + + /// + /// Reads from the socket until the array is filled, or no more data is coming(because + /// the socket closed or the timeout expired). + /// + /// A span of bytes to store the read data into. + /// The total number of bytes read. + /// This uses the default time out value. + int Read(Span data); +#else + /// + /// Sends the specified number of bytes of data to a , + /// + /// A array that acts as a buffer, containing the data to send. + void Send(byte[] data); + + /// + /// Reads from the socket until the array is filled, or no more data is coming(because + /// the socket closed or the timeout expired). + /// + /// The buffer to store the read data into. + /// The total number of bytes read. + /// This uses the default time out value. + int Read(byte[] data); +#endif + + /// /// Gets a that can be used to send and receive shell output to and /// from the device. @@ -134,5 +171,10 @@ public partial interface IAdbSocket : IDisposable /// The device to which to connect. /// If is , this method does nothing. void SetDevice(DeviceData device); + + /// + /// Closes the connection and releases all associated resources. + /// + void Close(); } } diff --git a/AdvancedSharpAdbClient/Interfaces/IDeviceMonitor.cs b/AdvancedSharpAdbClient/Interfaces/IDeviceMonitor.cs index 15072324..1fde9832 100644 --- a/AdvancedSharpAdbClient/Interfaces/IDeviceMonitor.cs +++ b/AdvancedSharpAdbClient/Interfaces/IDeviceMonitor.cs @@ -18,22 +18,27 @@ public partial interface IDeviceMonitor : IDisposable /// /// Occurs when the status of one of the connected devices has changed. /// - event EventHandler DeviceChanged; + event EventHandler? DeviceChanged; /// /// Occurs when received a list of device from the Android Debug Bridge. /// - event EventHandler DeviceNotified; + event EventHandler? DeviceNotified; /// /// Occurs when a device has connected to the Android Debug Bridge. /// - event EventHandler DeviceConnected; + event EventHandler? DeviceConnected; + + /// + /// Occurs when the list of the connected devices has changed. + /// + event EventHandler? DeviceListChanged; /// /// Occurs when a device has disconnected from the Android Debug Bridge. /// - event EventHandler DeviceDisconnected; + event EventHandler? DeviceDisconnected; /// /// Gets the devices that are currently connected to the Android Debug Bridge. diff --git a/AdvancedSharpAdbClient/Interfaces/ISyncService.Async.cs b/AdvancedSharpAdbClient/Interfaces/ISyncService.Async.cs index f6f8c18e..65d193a8 100644 --- a/AdvancedSharpAdbClient/Interfaces/ISyncService.Async.cs +++ b/AdvancedSharpAdbClient/Interfaces/ISyncService.Async.cs @@ -22,7 +22,7 @@ public partial interface ISyncService /// An optional parameter which, when specified, returns progress notifications. The progress is reported as a value between 0 and 100, representing the percentage of the file which has been transferred. /// A that can be used to cancel the task. /// A which represents the asynchronous operation. - Task PushAsync(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress, CancellationToken cancellationToken); + Task PushAsync(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress? progress, CancellationToken cancellationToken); /// /// Pulls (downloads) a file from the remote device. @@ -32,7 +32,7 @@ public partial interface ISyncService /// An optional parameter which, when specified, returns progress notifications. The progress is reported as a value between 0 and 100, representing the percentage of the file which has been transferred. /// A that can be used to cancel the task. /// A which represents the asynchronous operation. - Task PullAsync(string remotePath, Stream stream, IProgress progress, CancellationToken cancellationToken); + Task PullAsync(string remotePath, Stream stream, IProgress? progress, CancellationToken cancellationToken); /// /// Returns information about a file on the device. @@ -48,7 +48,7 @@ public partial interface ISyncService /// The path to the directory on the device. /// A that can be used to cancel the task. /// A which return for each child item of the directory, a object with information of the item. - Task> GetDirectoryListingAsync(string remotePath, CancellationToken cancellationToken); + Task> GetDirectoryListingAsync(string remotePath, CancellationToken cancellationToken); #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER /// @@ -66,6 +66,13 @@ public partial interface ISyncService /// A that can be used to cancel the task. /// A which represents the asynchronous operation. Task OpenAsync(CancellationToken cancellationToken); + + /// + /// Reopen this connection. Use this when the socket was disconnected by adb and you have restarted adb. + /// + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + Task ReopenAsync(CancellationToken cancellationToken = default); } } #endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Interfaces/ISyncService.cs b/AdvancedSharpAdbClient/Interfaces/ISyncService.cs index f06f60c9..c5763c9f 100644 --- a/AdvancedSharpAdbClient/Interfaces/ISyncService.cs +++ b/AdvancedSharpAdbClient/Interfaces/ISyncService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; namespace AdvancedSharpAdbClient { @@ -22,10 +21,6 @@ public partial interface ISyncService : IDisposable /// bool IsOpen { get; } -#if !HAS_TASK -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#pragma warning disable CS1574 // XML 注释中有未能解析的 cref 特性 -#endif /// /// Pushes (uploads) a file to the remote device. /// @@ -34,12 +29,8 @@ public partial interface ISyncService : IDisposable /// The permission octet that contains the permissions of the newly created file on the device. /// The time at which the file was last modified. /// An optional parameter which, when specified, returns progress notifications. The progress is reported as a value between 0 and 100, representing the percentage of the file which has been transferred. - /// A that can be used to cancel the task. - void Push(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress -#if HAS_TASK - , CancellationToken cancellationToken -#endif - ); + /// A that can be used to cancel the task. + void Push(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress? progress, in bool isCancelled); /// /// Pulls (downloads) a file from the remote device. @@ -47,16 +38,8 @@ void Push(Stream stream, string remotePath, int permissions, DateTimeOffset time /// The path, on the device, of the file to pull. /// A that will receive the contents of the file. /// An optional parameter which, when specified, returns progress notifications. The progress is reported as a value between 0 and 100, representing the percentage of the file which has been transferred. - /// A that can be used to cancel the task. - void Pull(string remotePath, Stream stream, IProgress progress -#if HAS_TASK - , CancellationToken cancellationToken -#endif - ); -#if !HAS_TASK -#pragma warning restore CS1574 // XML 注释中有未能解析的 cref 特性 -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif + /// A that can be used to cancel the task. + void Pull(string remotePath, Stream stream, IProgress? progress, in bool isCancelled); /// /// Returns information about a file on the device. @@ -77,9 +60,14 @@ void Pull(string remotePath, Stream stream, IProgress progress /// void Open(); + /// + /// Reopen this connection. Use this when the socket was disconnected by adb and you have restarted adb. + /// + void Reopen(); + /// /// Occurs when there is a change in the status of the sync. /// - event EventHandler SyncProgressChanged; + event EventHandler? SyncProgressChanged; } } diff --git a/AdvancedSharpAdbClient/Interfaces/ITcpSocket.Async.cs b/AdvancedSharpAdbClient/Interfaces/ITcpSocket.Async.cs index ef95917f..70e6441f 100644 --- a/AdvancedSharpAdbClient/Interfaces/ITcpSocket.Async.cs +++ b/AdvancedSharpAdbClient/Interfaces/ITcpSocket.Async.cs @@ -3,6 +3,8 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // +using System; +using System.Net; using System.Net.Sockets; using System.Threading; @@ -10,6 +12,53 @@ namespace AdvancedSharpAdbClient { public partial interface ITcpSocket { +#if NET6_0_OR_GREATER + /// + /// Begins an asynchronous request for a connection to a remote host. + /// + /// An that represents the remote device. + /// A which can be used to cancel the asynchronous task. + /// A that represents the asynchronous operation. + ValueTask ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken); + + /// + /// Re-establishes the connection to a remote host. Assumes you have resolved the reason that caused the + /// socket to disconnect. + /// + /// Force reconnect whatever the socket is connected or not. + /// A which can be used to cancel the asynchronous task. + /// A that represents the asynchronous operation. + ValueTask ReconnectAsync(bool isForce, CancellationToken cancellationToken); +#else + /// + /// Begins an asynchronous request for a connection to a remote host. + /// + /// An that represents the remote device. + /// A which can be used to cancel the asynchronous task. + /// A that represents the asynchronous operation. + Task ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken); + + /// + /// Re-establishes the connection to a remote host. Assumes you have resolved the reason that caused the + /// socket to disconnect. + /// + /// Force reconnect whatever the socket is connected or not. + /// A which can be used to cancel the asynchronous task. + /// A that represents the asynchronous operation. + Task ReconnectAsync(bool isForce, CancellationToken cancellationToken); +#endif + + /// + /// Asynchronously sends the specified number of bytes of data to a connected + /// using the specified . + /// + /// An array of type Byte that contains the data to be sent. + /// The number of bytes to send. + /// A bitwise combination of the SocketFlags values. + /// A which can be used to cancel the asynchronous task. + /// The number of bytes sent to the Socket. + Task SendAsync(byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken); + /// /// Asynchronously sends the specified number of bytes of data to a connected /// , starting at the specified , @@ -24,8 +73,20 @@ public partial interface ITcpSocket Task SendAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken); /// - /// Receives the specified number of bytes from a bound into the specified offset position of the - /// receive buffer, using the specified SocketFlags. + /// Receives the specified number of bytes from a bound + /// using the specified SocketFlags. + /// + /// An array of type Byte that is the storage location for received data. + /// The number of bytes to receive. + /// A bitwise combination of the SocketFlags values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + Task ReceiveAsync(byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken); + + /// + /// Receives the specified number of bytes from a bound + /// into the specified offset position of the receive buffer, using the specified SocketFlags. /// /// An array of type Byte that is the storage location for received data. /// The location in buffer to store the received data. @@ -35,6 +96,50 @@ public partial interface ITcpSocket /// Cancelling the task will also close the socket. /// The number of bytes received. Task ReceiveAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken); + +#if HAS_BUFFERS + /// + /// Asynchronously sends the specified number of bytes of data to a connected + /// using the specified . + /// + /// An array of type Byte that contains the data to be sent. + /// A bitwise combination of the SocketFlags values. + /// A which can be used to cancel the asynchronous task. + /// The number of bytes sent to the Socket. + public ValueTask SendAsync(ReadOnlyMemory buffer, SocketFlags socketFlags, CancellationToken cancellationToken); + + /// + /// Receives the specified number of bytes from a bound + /// using the specified SocketFlags. + /// + /// An array of type Byte that is the storage location for received data. + /// A bitwise combination of the SocketFlags values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public ValueTask ReceiveAsync(Memory buffer, SocketFlags socketFlags, CancellationToken cancellationToken); +#else + /// + /// Asynchronously sends the specified number of bytes of data to a connected + /// using the specified . + /// + /// An array of type Byte that contains the data to be sent. + /// A bitwise combination of the SocketFlags values. + /// A which can be used to cancel the asynchronous task. + /// The number of bytes sent to the Socket. + Task SendAsync(byte[] buffer, SocketFlags socketFlags, CancellationToken cancellationToken); + + /// + /// Receives the specified number of bytes from a bound + /// using the specified SocketFlags. + /// + /// An array of type Byte that is the storage location for received data. + /// A bitwise combination of the SocketFlags values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + Task ReceiveAsync(byte[] buffer, SocketFlags socketFlags, CancellationToken cancellationToken); +#endif } } #endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Interfaces/ITcpSocket.cs b/AdvancedSharpAdbClient/Interfaces/ITcpSocket.cs index eac68481..783513e0 100644 --- a/AdvancedSharpAdbClient/Interfaces/ITcpSocket.cs +++ b/AdvancedSharpAdbClient/Interfaces/ITcpSocket.cs @@ -18,7 +18,7 @@ public partial interface ITcpSocket : IDisposable { /// /// Gets a value indicating whether a is connected to a remote host as of the last - /// or operation. + /// or operation. /// bool Connected { get; } @@ -37,7 +37,18 @@ public partial interface ITcpSocket : IDisposable /// Re-establishes the connection to a remote host. Assumes you have resolved the reason that caused the /// socket to disconnect. /// - void Reconnect(); + /// Force reconnect whatever the socket is connected or not. + void Reconnect(bool isForce = false); + + /// + /// Sends the specified number of bytes of data to a connected + /// using the specified . + /// + /// An array of type Byte that contains the data to be sent. + /// The number of bytes to send. + /// A bitwise combination of the SocketFlags values. + /// The number of bytes sent to the Socket. + int Send(byte[] buffer, int size, SocketFlags socketFlags); /// /// Sends the specified number of bytes of data to a connected @@ -52,8 +63,8 @@ public partial interface ITcpSocket : IDisposable int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags); /// - /// Receives the specified number of bytes from a bound into the specified offset position of the - /// receive buffer, using the specified SocketFlags. + /// Receives the specified number of bytes from a bound + /// using the specified SocketFlags. /// /// An array of type Byte that is the storage location for received data. /// The number of bytes to receive. @@ -61,10 +72,64 @@ public partial interface ITcpSocket : IDisposable /// The number of bytes received. int Receive(byte[] buffer, int size, SocketFlags socketFlags); + /// + /// Receives the specified number of bytes from a bound + /// into the specified offset position of the receive buffer, using the specified SocketFlags. + /// + /// An array of type Byte that is the storage location for received data. + /// The position in the parameter to store the received data. + /// The number of bytes to receive. + /// A bitwise combination of the SocketFlags values. + /// The number of bytes received. + int Receive(byte[] buffer, int offset, int size, SocketFlags socketFlags); + +#if HAS_BUFFERS + /// + /// Sends the specified number of bytes of data to a connected + /// using the specified . + /// + /// A span of bytes that contains the data to be sent. + /// A bitwise combination of the SocketFlags values. + /// The number of bytes sent to the Socket. + int Send(ReadOnlySpan buffer, SocketFlags socketFlags); + + /// + /// Receives the specified number of bytes from a bound + /// using the specified SocketFlags. + /// + /// A span of bytes that is the storage location for the received data. + /// A bitwise combination of the SocketFlags values. + /// The number of bytes received. + int Receive(Span buffer, SocketFlags socketFlags); +#else + /// + /// Sends the specified number of bytes of data to a connected + /// using the specified . + /// + /// An array of type Byte that contains the data to be sent. + /// A bitwise combination of the SocketFlags values. + /// The number of bytes sent to the Socket. + int Send(byte[] buffer, SocketFlags socketFlags); + + /// + /// Receives the specified number of bytes from a bound + /// using the specified SocketFlags. + /// + /// An array of type Byte that is the storage location for received data. + /// A bitwise combination of the SocketFlags values. + /// The number of bytes received. + int Receive(byte[] buffer, SocketFlags socketFlags); +#endif + /// /// Gets the underlying . /// /// The underlying stream. Stream GetStream(); + + /// + /// Closes the connection and releases all associated resources. + /// + void Close(); } } diff --git a/AdvancedSharpAdbClient/Logs/AndroidLogEntry.cs b/AdvancedSharpAdbClient/Logs/AndroidLogEntry.cs index 9cf930ab..1531cf5a 100644 --- a/AdvancedSharpAdbClient/Logs/AndroidLogEntry.cs +++ b/AdvancedSharpAdbClient/Logs/AndroidLogEntry.cs @@ -15,7 +15,7 @@ public class AndroidLogEntry : LogEntry /// /// Maps Android log priorities to chars used to represent them in the system log. /// - private static readonly Dictionary PriorityFormatters = new() + private static readonly Dictionary PriorityFormatters = new(6) { { Priority.Verbose, 'V' }, { Priority.Debug, 'D' }, @@ -25,6 +25,11 @@ public class AndroidLogEntry : LogEntry { Priority.Assert, 'A' } }; + /// + /// Initializes a new instance of the class. + /// + public AndroidLogEntry() { } + /// /// Gets or sets the priority of the log message. /// @@ -34,12 +39,12 @@ public class AndroidLogEntry : LogEntry /// Gets or sets the log tag of the message. Used to identify the source of a log message. /// It usually identifies the class or activity where the log call occurred. /// - public string Tag { get; set; } + public string Tag { get; set; } = string.Empty; /// /// Gets or sets the message that has been logged. /// - public string Message { get; set; } + public string Message { get; set; } = string.Empty; /// public override string ToString() => @@ -51,6 +56,6 @@ public override string ToString() => /// The value to convert. /// A that represents in the system log. private static char FormatPriority(Priority value) => - PriorityFormatters?.TryGetValue(value, out var result) == true ? result : '?'; + PriorityFormatters.TryGetValue(value, out char result) == true ? result : '?'; } } diff --git a/AdvancedSharpAdbClient/Logs/Enums/LogLevel.cs b/AdvancedSharpAdbClient/Logs/Enums/LogLevel.cs new file mode 100644 index 00000000..0578c31c --- /dev/null +++ b/AdvancedSharpAdbClient/Logs/Enums/LogLevel.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +namespace AdvancedSharpAdbClient.Logs +{ + /// + /// Defines logging severity levels. + /// + public enum LogLevel : byte + { + /// + /// Logs that contain the most detailed messages. These messages may contain sensitive application data. + /// These messages are disabled by default and should never be enabled in a production environment. + /// + Trace = 0, + + /// + /// Logs that are used for interactive investigation during development. These logs should primarily contain + /// information useful for debugging and have no long-term value. + /// + Debug = 1, + + /// + /// Logs that track the general flow of the application. These logs should have long-term value. + /// + Information = 2, + + /// + /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the + /// application execution to stop. + /// + Warning = 3, + + /// + /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a + /// failure in the current activity, not an application-wide failure. + /// + Error = 4, + + /// + /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires + /// immediate attention. + /// + Critical = 5, + + /// + /// Not used for writing log messages. Specifies that a logging category should not write any messages. + /// + None = 6, + } +} diff --git a/AdvancedSharpAdbClient/Logs/EventLogEntry.cs b/AdvancedSharpAdbClient/Logs/EventLogEntry.cs index 5e7d5c4c..2b7de5ea 100644 --- a/AdvancedSharpAdbClient/Logs/EventLogEntry.cs +++ b/AdvancedSharpAdbClient/Logs/EventLogEntry.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using System.Collections.ObjectModel; +using System.Collections.Generic; namespace AdvancedSharpAdbClient.Logs { @@ -12,6 +12,11 @@ namespace AdvancedSharpAdbClient.Logs /// public class EventLogEntry : LogEntry { + /// + /// Initializes a new instance of the class. + /// + public EventLogEntry() { } + /// /// Gets or sets the 4 bytes integer key from "/system/etc/event-log-tags" file. /// @@ -20,6 +25,6 @@ public class EventLogEntry : LogEntry /// /// Gets or sets the values of this event log entry. /// - public Collection Values { get; set; } = new Collection(); + public List Values { get; set; } = []; } } diff --git a/AdvancedSharpAdbClient/Logs/Interfaces/ILogger.cs b/AdvancedSharpAdbClient/Logs/Interfaces/ILogger.cs new file mode 100644 index 00000000..cd305358 --- /dev/null +++ b/AdvancedSharpAdbClient/Logs/Interfaces/ILogger.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; + +namespace AdvancedSharpAdbClient.Logs +{ + /// + /// Represents a type used to perform logging. + /// + /// Aggregates most logging patterns to a single method. + public interface ILogger + { + /// + /// Formats and writes a log message at the specified log level. + /// + /// Entry will be written on this level. + /// The exception to log. + /// Format string of the log message. + /// An object array that contains zero or more objects to format. + void Log(LogLevel logLevel, Exception? exception, string? message, params object?[] args); + } + + /// + /// A generic interface for logging where the category name is derived from the specified + /// type name. + /// Generally used to enable activation of a named from dependency injection. + /// + /// The type whose name is used for the logger category name. + public interface ILogger : ILogger + { + + } +} diff --git a/AdvancedSharpAdbClient/Logs/Interfaces/ILoggerFactory.cs b/AdvancedSharpAdbClient/Logs/Interfaces/ILoggerFactory.cs new file mode 100644 index 00000000..7228f002 --- /dev/null +++ b/AdvancedSharpAdbClient/Logs/Interfaces/ILoggerFactory.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +namespace AdvancedSharpAdbClient.Logs +{ + /// + /// Represents a type used to configure the logging system and create instances of . + /// + public interface ILoggerFactory + { + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// The . + ILogger CreateLogger(string categoryName); + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// The . + ILogger CreateLogger(); + } +} diff --git a/AdvancedSharpAdbClient/Logs/LogEntry.cs b/AdvancedSharpAdbClient/Logs/LogEntry.cs index 7076368e..4c232140 100644 --- a/AdvancedSharpAdbClient/Logs/LogEntry.cs +++ b/AdvancedSharpAdbClient/Logs/LogEntry.cs @@ -14,6 +14,11 @@ namespace AdvancedSharpAdbClient.Logs /// public class LogEntry { + /// + /// Initializes a new instance of the class. + /// + public LogEntry() { } + /// /// Gets or sets the process ID of the code that generated the log message. /// @@ -43,6 +48,6 @@ public class LogEntry /// /// Gets or sets the entry's payload. /// - public byte[] Data { get; set; } + public byte[] Data { get; set; } = []; } } diff --git a/AdvancedSharpAdbClient/Logs/LogReader.Async.cs b/AdvancedSharpAdbClient/Logs/LogReader.Async.cs index 20ee8884..d95fb309 100644 --- a/AdvancedSharpAdbClient/Logs/LogReader.Async.cs +++ b/AdvancedSharpAdbClient/Logs/LogReader.Async.cs @@ -3,7 +3,6 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.IO; using System.Text; @@ -18,7 +17,7 @@ public partial class LogReader /// /// A which can be used to cancel the asynchronous operation. /// A which return a new object. - public async Task ReadEntryAsync(CancellationToken cancellationToken = default) + public async Task ReadEntryAsync(CancellationToken cancellationToken = default) { // Read the log data in binary format. This format is defined at // https://android.googlesource.com/platform/system/core/+/master/include/log/logger.h @@ -35,11 +34,11 @@ public async Task ReadEntryAsync(CancellationToken cancellationToken = return null; } - ushort payloadLength = payloadLengthValue.Value; - ushort headerSize = headerSizeValue.Value; - int pid = pidValue.Value; - int tid = tidValue.Value; - int sec = secValue.Value; + ushort payloadLength = payloadLengthValue!.Value; + ushort headerSize = headerSizeValue!.Value; + int pid = pidValue!.Value; + int tid = tidValue!.Value; + int sec = secValue!.Value; int nsec = nsecValue.Value; // If the headerSize is not 0, we have on of the logger_entry_v* objects. @@ -87,14 +86,14 @@ public async Task ReadEntryAsync(CancellationToken cancellationToken = } } - byte[] data = await ReadBytesSafeAsync(payloadLength, cancellationToken).ConfigureAwait(false); + byte[]? data = await ReadBytesSafeAsync(payloadLength, cancellationToken).ConfigureAwait(false); if (data == null) { return null; } - DateTimeOffset timestamp = Utilities.FromUnixTimeSeconds(sec); + DateTimeOffset timestamp = DateTimeExtensions.FromUnixTimeSeconds(sec); switch ((LogId)id) { @@ -153,7 +152,7 @@ or LogId.Radio #if NETCOREAPP3_0_OR_GREATER await #endif - using (MemoryStream dataStream = new(data)) + using (MemoryStream dataStream = new(data)) { using BinaryReader reader = new(dataStream); int priority = reader.ReadInt32(); @@ -181,41 +180,37 @@ or LogId.Radio } private async Task ReadUInt16Async(CancellationToken cancellationToken = default) { - byte[] data = await ReadBytesSafeAsync(2, cancellationToken).ConfigureAwait(false); + byte[]? data = await ReadBytesSafeAsync(2, cancellationToken).ConfigureAwait(false); return data == null ? null : BitConverter.ToUInt16(data, 0); } private async Task ReadUInt32Async(CancellationToken cancellationToken = default) { - byte[] data = await ReadBytesSafeAsync(4, cancellationToken).ConfigureAwait(false); + byte[]? data = await ReadBytesSafeAsync(4, cancellationToken).ConfigureAwait(false); return data == null ? null : BitConverter.ToUInt32(data, 0); } private async Task ReadInt32Async(CancellationToken cancellationToken = default) { - byte[] data = await ReadBytesSafeAsync(4, cancellationToken).ConfigureAwait(false); + byte[]? data = await ReadBytesSafeAsync(4, cancellationToken).ConfigureAwait(false); return data == null ? null : BitConverter.ToInt32(data, 0); } - private async Task ReadBytesSafeAsync(int count, CancellationToken cancellationToken = default) + private async Task ReadBytesSafeAsync(int count, CancellationToken cancellationToken = default) { int totalRead = 0; int read = 0; byte[] data = new byte[count]; - while ((read = -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await stream.ReadAsync(data.AsMemory(totalRead, count - totalRead), cancellationToken).ConfigureAwait(false) -#elif !NET35 - await stream.ReadAsync(data, totalRead, count - totalRead, cancellationToken).ConfigureAwait(false) +#if HAS_BUFFERS + while ((read = await stream.ReadAsync(data.AsMemory(totalRead, count - totalRead), cancellationToken).ConfigureAwait(false)) > 0) #else - await Utilities.Run(() => stream.Read(data, totalRead, count - totalRead)).ConfigureAwait(false) + while ((read = await stream.ReadAsync(data, totalRead, count - totalRead, cancellationToken).ConfigureAwait(false)) > 0) #endif - ) > 0) { totalRead += read; } diff --git a/AdvancedSharpAdbClient/Logs/LogReader.cs b/AdvancedSharpAdbClient/Logs/LogReader.cs index dec055bd..35dbf9d5 100644 --- a/AdvancedSharpAdbClient/Logs/LogReader.cs +++ b/AdvancedSharpAdbClient/Logs/LogReader.cs @@ -2,9 +2,8 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.IO; using System.Text; @@ -13,24 +12,19 @@ namespace AdvancedSharpAdbClient.Logs /// /// Processes Android log files in binary format. You usually get the binary format by running logcat -B. /// - public partial class LogReader + /// A that contains the logcat data. + public partial class LogReader(Stream stream) { /// /// The that contains the logcat data. /// - private readonly Stream stream; - - /// - /// Initializes a new instance of the class. - /// - /// A that contains the logcat data. - public LogReader(Stream stream) => this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + private readonly Stream stream = stream ?? throw new ArgumentNullException(nameof(stream)); /// /// Reads the next from the stream. /// /// A new object. - public virtual LogEntry ReadEntry() + public virtual LogEntry? ReadEntry() { // Read the log data in binary format. This format is defined at // https://android.googlesource.com/platform/system/core/+/master/include/log/logger.h @@ -47,11 +41,11 @@ public virtual LogEntry ReadEntry() return null; } - ushort payloadLength = payloadLengthValue.Value; - ushort headerSize = headerSizeValue.Value; - int pid = pidValue.Value; - int tid = tidValue.Value; - int sec = secValue.Value; + ushort payloadLength = payloadLengthValue!.Value; + ushort headerSize = headerSizeValue!.Value; + int pid = pidValue!.Value; + int tid = tidValue!.Value; + int sec = secValue!.Value; int nsec = nsecValue.Value; // If the headerSize is not 0, we have on of the logger_entry_v* objects. @@ -59,7 +53,6 @@ public virtual LogEntry ReadEntry() // header size and payload length. // For both objects, the size should be 0x18 uint id = 0; - uint uid = 0; if (headerSize != 0) { @@ -84,7 +77,7 @@ public virtual LogEntry ReadEntry() return null; } - uid = uidValue.Value; + _ = uidValue.Value; } if (headerSize >= 0x20) @@ -99,14 +92,14 @@ public virtual LogEntry ReadEntry() } } - byte[] data = ReadBytesSafe(payloadLength); + byte[]? data = ReadBytesSafe(payloadLength); if (data == null) { return null; } - DateTimeOffset timestamp = Utilities.FromUnixTimeSeconds(sec); + DateTimeOffset timestamp = DateTimeExtensions.FromUnixTimeSeconds(sec); switch ((LogId)id) { @@ -192,7 +185,7 @@ or LogId.Radio /// /// Reads a single log entry from the stream. /// - protected void ReadLogEntry(BinaryReader reader, Collection parent) + protected void ReadLogEntry(BinaryReader reader, ICollection parent) { EventLogType type = (EventLogType)reader.ReadByte(); @@ -213,7 +206,7 @@ protected void ReadLogEntry(BinaryReader reader, Collection parent) case EventLogType.List: byte listLength = reader.ReadByte(); - Collection list = new(); + List list = []; for (int i = 0; i < listLength; i++) { @@ -237,9 +230,14 @@ protected void ReadLogEntry(BinaryReader reader, Collection parent) /// protected ushort? ReadUInt16() { - byte[] data = ReadBytesSafe(2); - - return data == null ? null : BitConverter.ToUInt16(data, 0); + byte[]? data = ReadBytesSafe(2); + + return data == null ? null +#if HAS_BUFFERS + : BitConverter.ToUInt16(data); +#else + : BitConverter.ToUInt16(data, 0); +#endif } /// @@ -247,9 +245,14 @@ protected void ReadLogEntry(BinaryReader reader, Collection parent) /// protected uint? ReadUInt32() { - byte[] data = ReadBytesSafe(4); - - return data == null ? null : BitConverter.ToUInt32(data, 0); + byte[]? data = ReadBytesSafe(4); + + return data == null ? null +#if HAS_BUFFERS + : BitConverter.ToUInt32(data); +#else + : BitConverter.ToUInt32(data, 0); +#endif } /// @@ -257,22 +260,31 @@ protected void ReadLogEntry(BinaryReader reader, Collection parent) /// protected int? ReadInt32() { - byte[] data = ReadBytesSafe(4); - - return data == null ? null : BitConverter.ToInt32(data, 0); + byte[]? data = ReadBytesSafe(4); + + return data == null ? null +#if HAS_BUFFERS + : BitConverter.ToInt32(data); +#else + : BitConverter.ToInt32(data, 0); +#endif } /// /// Reads bytes from the stream, making sure that the requested number of bytes /// /// The number of bytes to read. - protected byte[] ReadBytesSafe(int count) + protected byte[]? ReadBytesSafe(int count) { int totalRead = 0; byte[] data = new byte[count]; int read; +#if HAS_BUFFERS + while ((read = stream.Read(data.AsSpan(totalRead, count - totalRead))) > 0) +#else while ((read = stream.Read(data, totalRead, count - totalRead)) > 0) +#endif { totalRead += read; } diff --git a/AdvancedSharpAdbClient/Logs/LoggerProvider.cs b/AdvancedSharpAdbClient/Logs/LoggerProvider.cs new file mode 100644 index 00000000..2f8c10ba --- /dev/null +++ b/AdvancedSharpAdbClient/Logs/LoggerProvider.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +namespace AdvancedSharpAdbClient.Logs +{ + /// + /// Provides a mechanism for creating instances of and classes. + /// + public static class LoggerProvider + { + private static ILoggerFactory? _loggerFactory = null; + + /// + /// Sets the current log provider based on logger factory. + /// + /// The logger factory. + public static void SetLogProvider(ILoggerFactory? loggerFactory) => _loggerFactory = loggerFactory; + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// A new instance. + public static ILogger CreateLogger(string category) => _loggerFactory == null ? NullLogger.Instance : _loggerFactory.CreateLogger(category); + + /// + /// Creates a new instance using the full name of the given type. + /// + /// The type. + /// The that was created + public static ILogger CreateLogger() => _loggerFactory == null ? NullLogger.Instance : _loggerFactory.CreateLogger(); + } +} diff --git a/AdvancedSharpAdbClient/Logs/NamespaceDoc.cs b/AdvancedSharpAdbClient/Logs/NamespaceDoc.cs index 52b00fbf..10619948 100644 --- a/AdvancedSharpAdbClient/Logs/NamespaceDoc.cs +++ b/AdvancedSharpAdbClient/Logs/NamespaceDoc.cs @@ -2,6 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // +using System.ComponentModel; using System.Runtime.CompilerServices; namespace AdvancedSharpAdbClient.Logs @@ -11,5 +12,6 @@ namespace AdvancedSharpAdbClient.Logs /// /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. [CompilerGenerated] - internal class NamespaceDoc { } + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : AdvancedSharpAdbClient.NamespaceDoc { } } diff --git a/AdvancedSharpAdbClient/Logs/NullLogger.cs b/AdvancedSharpAdbClient/Logs/NullLogger.cs new file mode 100644 index 00000000..48fa3bc5 --- /dev/null +++ b/AdvancedSharpAdbClient/Logs/NullLogger.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; + +namespace AdvancedSharpAdbClient.Logs +{ + /// + /// Minimalistic logger that does nothing. + /// + public class NullLogger : ILogger + { + /// + /// Returns the shared instance of . + /// + public static NullLogger Instance { get; } = new(); + + /// + public void Log(LogLevel logLevel, Exception? exception, string? message, params object?[] args) { } + } + + /// + /// Minimalistic logger that does nothing. + /// + public class NullLogger : NullLogger, ILogger + { + /// + /// Returns an instance of . + /// + /// An instance of . + public static new NullLogger Instance { get; } = new(); + } +} diff --git a/AdvancedSharpAdbClient/Logs/NullLoggerT.cs b/AdvancedSharpAdbClient/Logs/NullLoggerT.cs deleted file mode 100644 index 8d60373b..00000000 --- a/AdvancedSharpAdbClient/Logs/NullLoggerT.cs +++ /dev/null @@ -1,55 +0,0 @@ -#if HAS_OLDLOGGER -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Logging; -using System; - -namespace Microsoft.Extensions.Logging.Abstractions -{ - /// - /// Minimalistic logger that does nothing. - /// - public class NullLogger : ILogger - { - /// - /// Returns an instance of . - /// - /// An instance of . - public static readonly NullLogger Instance = new(); - - /// - public IDisposable BeginScope(TState state) => NullScope.Instance; - - /// - /// - /// This method ignores the parameters and does nothing. - /// - public void Log( - LogLevel logLevel, - EventId eventId, - TState state, - Exception exception, - Func formatter) - { - } - - /// - public bool IsEnabled(LogLevel logLevel) => false; - } - - internal sealed class NullScope : IDisposable - { - public static NullScope Instance { get; } = new NullScope(); - - private NullScope() - { - } - - /// - public void Dispose() - { - } - } -} -#endif diff --git a/AdvancedSharpAdbClient/Models/AdbResponse.cs b/AdvancedSharpAdbClient/Models/AdbResponse.cs index 5a89ddb0..6b6013e5 100644 --- a/AdvancedSharpAdbClient/Models/AdbResponse.cs +++ b/AdvancedSharpAdbClient/Models/AdbResponse.cs @@ -3,27 +3,33 @@ // using System; +using System.Diagnostics.CodeAnalysis; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// - /// An Adb Communication Response. + /// The response returned by ADB server. /// - public class AdbResponse + public readonly struct AdbResponse : IEquatable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// public AdbResponse() => Message = string.Empty; + /// + /// Initializes a new instance of the struct. + /// + /// the message of . + public AdbResponse(string message) => Message = message; + /// /// Gets a that represents the OK response sent by ADB. /// - public static AdbResponse OK { get; } = new AdbResponse() + public static AdbResponse OK { get; } = new() { IOSuccess = true, Okay = true, - Message = string.Empty, Timeout = false }; @@ -31,25 +37,36 @@ public class AdbResponse /// Gets or sets a value indicating whether the IO communication was a success. /// /// if successful; otherwise, . - public bool IOSuccess { get; set; } + public bool IOSuccess { get; init; } /// /// Gets or sets a value indicating whether this is okay. /// /// if okay; otherwise, . - public bool Okay { get; set; } + public bool Okay { get; init; } /// /// Gets or sets a value indicating whether this is timeout. /// /// if timeout; otherwise, . - public bool Timeout { get; set; } + public bool Timeout { get; init; } /// /// Gets or sets the message. /// /// The message. - public string Message { get; set; } + public string Message { get; init; } + + /// + /// Throw if or is . + /// + public void Throw() + { + if (!IOSuccess || !Okay) + { + throw new AdbException($"An error occurred while reading a response from ADB: {Message}", this); + } + } /// /// Creates a new instance of the class, based on an @@ -57,45 +74,46 @@ public class AdbResponse /// /// The error message returned by adb. /// A new object that represents the error. - public static AdbResponse FromError(string message) => new() + public static AdbResponse FromError(string message) => new(message) { IOSuccess = true, - Message = message, Okay = false, Timeout = false }; - /// - /// Determines whether the specified is equal to the current object. - /// - /// The to compare with the current object. - /// if the specified object is equal to the current object; otherwise, . - public override bool Equals(object obj) => - obj is AdbResponse other - && other.IOSuccess == IOSuccess - && string.Equals(other.Message, Message, StringComparison.OrdinalIgnoreCase) - && other.Okay == Okay - && other.Timeout == Timeout; + /// + public override bool Equals([NotNullWhen(true)] object? obj) => obj is AdbResponse other && Equals(other); - /// - /// Gets the hash code for the current . - /// - /// A hash code for the current . - public override int GetHashCode() - { - int hash = 17; - hash = (hash * 23) + IOSuccess.GetHashCode(); - hash = (hash * 23) + Message == null ? 0 : Message.GetHashCode(); - hash = (hash * 23) + Okay.GetHashCode(); - hash = (hash * 23) + Timeout.GetHashCode(); + /// + public bool Equals(AdbResponse other) => + other.IOSuccess == IOSuccess + && string.Equals(other.Message, Message, StringComparison.OrdinalIgnoreCase) + && other.Okay == Okay + && other.Timeout == Timeout; - return hash; - } + /// + public override int GetHashCode() => HashCode.Combine(IOSuccess, Message, Okay, Timeout); /// /// Returns a that represents the current . /// /// OK if the response is an OK response, or Error: {Message} if the response indicates an error. public override string ToString() => Equals(OK) ? "OK" : $"Error: {Message}"; + + /// + /// Tests whether two objects are equally. + /// + /// The structure that is to the left of the equality operator. + /// The structure that is to the right of the equality operator. + /// This operator returns if the two structures are equally; otherwise . + public static bool operator ==(AdbResponse left, AdbResponse right) => left.Equals(right); + + /// + /// Tests whether two objects are different. + /// + /// The structure that is to the left of the inequality operator. + /// The structure that is to the right of the inequality operator. + /// This operator returns if the two structures are unequally; otherwise . + public static bool operator !=(AdbResponse left, AdbResponse right) => !left.Equals(right); } } diff --git a/AdvancedSharpAdbClient/Models/AdbServerStatus.cs b/AdvancedSharpAdbClient/Models/AdbServerStatus.cs index 1caa6bf7..dbb2030b 100644 --- a/AdvancedSharpAdbClient/Models/AdbServerStatus.cs +++ b/AdvancedSharpAdbClient/Models/AdbServerStatus.cs @@ -4,40 +4,31 @@ using System; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Represents the status of the adb server. /// - public struct AdbServerStatus + /// The value indicating whether the server is currently running. + /// The version of the server when it is running. + public readonly record struct AdbServerStatus(bool IsRunning, Version? Version) { /// /// Gets a value indicating whether the server is currently running. /// - public bool IsRunning { get; internal set; } + public bool IsRunning { get; init; } = IsRunning; /// /// Gets the version of the server when it is running. /// - public Version Version { get; internal set; } - - /// - /// Initializes a new instance of the struct. - /// - /// The value indicating whether the server is currently running. - /// The version of the server when it is running. - public AdbServerStatus(bool isRunning, Version version) - { - IsRunning = isRunning; - Version = version; - } + public Version? Version { get; init; } = Version; /// /// Deconstruct the struct. /// /// The value indicating whether the server is currently running. /// The version of the server when it is running. - public readonly void Deconstruct(out bool isRunning, out Version version) + public readonly void Deconstruct(out bool isRunning, out Version? version) { isRunning = IsRunning; version = Version; diff --git a/AdvancedSharpAdbClient/Models/Area.cs b/AdvancedSharpAdbClient/Models/Area.cs deleted file mode 100644 index 1fc55e52..00000000 --- a/AdvancedSharpAdbClient/Models/Area.cs +++ /dev/null @@ -1,592 +0,0 @@ -using System; - -namespace AdvancedSharpAdbClient -{ - /// - /// Stores the location and size of a rectangular region. - /// - public struct Area : IEquatable - { - /// - /// Represents a structure with its properties left uninitialized. - /// - public static readonly Area Empty; - - private int x; // Do not rename (binary serialization) - private int y; // Do not rename (binary serialization) - private int width; // Do not rename (binary serialization) - private int height; // Do not rename (binary serialization) - - /// - /// Initializes a new instance of the class with the specified location - /// and size. - /// - /// The x-coordinate of the upper-left corner of the area. - /// The y-coordinate of the upper-left corner of the area. - /// The width of the area. - /// The height of the area. - public Area(int x, int y, int width, int height) - { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - -#if HAS_DRAWING - /// - /// Initializes a new instance of the class with the specified rectangle. - /// - /// A that represents the rectangular region. - public Area(Rectangle rectangle) - { - x = rectangle.X; - y = rectangle.Y; - width = rectangle.Width; - height = rectangle.Height; - } - - /// - /// Initializes a new instance of the class with the specified location and size. - /// - /// A that represents the upper-left corner of the rectangular region. - /// A that represents the width and height of the rectangular region. - public Area(Cords location, System.Drawing.Size size) - { - x = location.X; - y = location.Y; - width = size.Width; - height = size.Height; - } - - /// - /// Initializes a new instance of the class with the specified location and size. - /// - /// A that represents the upper-left corner of the rectangular region. - /// A that represents the width and height of the rectangular region. - public Area(System.Drawing.Point location, System.Drawing.Size size) - { - x = location.X; - y = location.Y; - width = size.Width; - height = size.Height; - } -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER -#pragma warning disable CS0419 // cref 特性中有不明确的引用 - /// - /// Initializes a new instance of the class with the specified rectangle. - /// - /// A that represents the rectangular region. - public Area(Rect rectangle) - { - x = unchecked((int)rectangle.X); - y = unchecked((int)rectangle.Y); - width = unchecked((int)rectangle.Width); - height = unchecked((int)rectangle.Height); - } - - /// - /// Initializes a new instance of the class with the specified location and size. - /// - /// A that represents the upper-left corner of the rectangular region. - /// A that represents the width and height of the rectangular region. - public Area(Cords location, Windows.Foundation.Size size) - { - x = location.X; - y = location.Y; - width = unchecked((int)size.Width); - height = unchecked((int)size.Height); - } - - /// - /// Initializes a new instance of the class with the specified location and size. - /// - /// A that represents the upper-left corner of the rectangular region. - /// A that represents the width and height of the rectangular region. - public Area(Windows.Foundation.Point location, Windows.Foundation.Size size) - { - x = unchecked((int)location.X); - y = unchecked((int)location.Y); - width = unchecked((int)size.Width); - height = unchecked((int)size.Height); - } -#pragma warning restore CS0419 // cref 特性中有不明确的引用 -#endif - - /// - /// Creates a new with the specified location and size. - /// - /// The x-coordinate of the upper-left corner of this structure. - /// The y-coordinate of the upper-left corner of this structure. - /// The x-coordinate of the lower-right corner of this structure. - /// The y-coordinate of the lower-right corner of this structure. - /// The new that this method creates. - public static Area FromLTRB(int left, int top, int right, int bottom) => - new(left, top, unchecked(right - left), unchecked(bottom - top)); - - /// - /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this - /// . - /// - public Cords Location - { - readonly get => new(X, Y); - set - { - X = value.X; - Y = value.Y; - } - } - - /// - /// Gets or sets the coordinates of the center of the rectangular region represented by this - /// . - /// - public readonly Cords Center => unchecked(new(X + (Width / 2), Y + (Height / 2))); - -#if HAS_DRAWING - /// - /// Gets or sets the size of this . - /// - public System.Drawing.Size Size - { - readonly get => new(Width, Height); - set - { - Width = value.Width; - Height = value.Height; - } - } -#endif - -#if !HAS_DRAWING && WINDOWS_UWP - /// - /// Gets or sets the size of this . - /// - public Size Size - { - readonly get => new(Width, Height); - set - { - Width = unchecked((int)value.Width); - Height = unchecked((int)value.Height); - } - } -#endif - - /// - /// Gets or sets the x-coordinate of the upper-left corner of the rectangular region defined by this - /// . - /// - public int X - { - readonly get => x; - set => x = value; - } - - /// - /// Gets or sets the y-coordinate of the upper-left corner of the rectangular region defined by this - /// . - /// - public int Y - { - readonly get => y; - set => y = value; - } - - /// - /// Gets or sets the width of the rectangular region defined by this . - /// - public int Width - { - readonly get => width; - set => width = value; - } - - /// - /// Gets or sets the width of the rectangular region defined by this . - /// - public int Height - { - readonly get => height; - set => height = value; - } - - /// - /// Gets the x-coordinate of the upper-left corner of the rectangular region defined by this - /// . - /// - public readonly int Left => X; - - /// - /// Gets the y-coordinate of the upper-left corner of the rectangular region defined by this - /// . - /// - public readonly int Top => Y; - - /// - /// Gets the x-coordinate of the lower-right corner of the rectangular region defined by this - /// . - /// - public readonly int Right => unchecked(X + Width); - - /// - /// Gets the y-coordinate of the lower-right corner of the rectangular region defined by this - /// . - /// - public readonly int Bottom => unchecked(Y + Height); - - /// - /// Tests whether this has a - /// or a of 0. - /// - public readonly bool IsEmpty => height == 0 && width == 0 && x == 0 && y == 0; - - /// - /// Tests whether is a with the same location - /// and size of this Area. - /// - /// The to test. - /// This method returns if is a structure - /// and its , , , and properties are equal to - /// the corresponding properties of this structure; otherwise, . - public override readonly bool Equals(object obj) => obj is Area area && Equals(area); - - /// - public readonly bool Equals(Area other) => this == other; - -#if HAS_DRAWING - /// - /// Creates a with the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator Rectangle(Area rect) => new(rect.X, rect.Y, rect.Width, rect.Height); - - /// - /// Creates a with the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator RectangleF(Area rect) => new(rect.X, rect.Y, rect.Width, rect.Height); - - /// - /// Creates a with the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator Area(Rectangle rect) => new(rect); -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER -#pragma warning disable CS0419 // cref 特性中有不明确的引用 - /// - /// Creates a with the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator Rect(Area rect) => new(rect.X, rect.Y, rect.Width, rect.Height); - - /// - /// Creates a with the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator Area(Rect rect) => new(rect); -#pragma warning restore CS0419 // cref 特性中有不明确的引用 -#endif - - /// - /// Tests whether two objects have equal location and size. - /// - /// The Rectangle structure that is to the left of the equality operator. - /// The Rectangle structure that is to the right of the equality operator. - /// This operator returns if the two structures have equal - /// , , , and properties. - public static bool operator ==(Area left, Area right) => - left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height; - - /// - /// Tests whether two objects differ in location or size. - /// - /// The Rectangle structure that is to the left of the inequality operator. - /// The Rectangle structure that is to the right of the inequality operator. - /// This operator returns if any of the , , - /// properties of the two structures are unequal; otherwise . - public static bool operator !=(Area left, Area right) => !(left == right); - -#if HAS_DRAWING - /// - /// Converts a to a by performing a ceiling operation on all the coordinates. - /// - /// The structure to be converted. - public static Area Ceiling(RectangleF value) - { - unchecked - { - return new Area( - (int)Math.Ceiling(value.X), - (int)Math.Ceiling(value.Y), - (int)Math.Ceiling(value.Width), - (int)Math.Ceiling(value.Height)); - } - } - - /// - /// Converts a to a by performing a truncate operation on all the coordinates. - /// - /// The structure to be converted. - public static Area Truncate(RectangleF value) - { - unchecked - { - return new Area( - (int)value.X, - (int)value.Y, - (int)value.Width, - (int)value.Height); - } - } - - /// - /// Converts a to a by performing a round operation on all the coordinates. - /// - /// The structure to be converted. - public static Area Round(RectangleF value) - { - unchecked - { - return new Area( - (int)Math.Round(value.X), - (int)Math.Round(value.Y), - (int)Math.Round(value.Width), - (int)Math.Round(value.Height)); - } - } -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER -#pragma warning disable CS0419 // cref 特性中有不明确的引用 - /// - /// Converts a to a by performing a ceiling operation on all the coordinates. - /// - /// The structure to be converted. - public static Area Ceiling(Rect value) - { - unchecked - { - return new Area( - (int)Math.Ceiling(value.X), - (int)Math.Ceiling(value.Y), - (int)Math.Ceiling(value.Width), - (int)Math.Ceiling(value.Height)); - } - } - - /// - /// Converts a to a by performing a truncate operation on all the coordinates. - /// - /// The structure to be converted. - public static Area Truncate(Rect value) - { - unchecked - { - return new Area( - (int)value.X, - (int)value.Y, - (int)value.Width, - (int)value.Height); - } - } - - /// - /// Converts a to a by performing a round operation on all the coordinates. - /// - /// The structure to be converted. - public static Area Round(Rect value) - { - unchecked - { - return new Area( - (int)Math.Round(value.X), - (int)Math.Round(value.Y), - (int)Math.Round(value.Width), - (int)Math.Round(value.Height)); - } - } -#pragma warning restore CS0419 // cref 特性中有不明确的引用 -#endif - - /// - /// Determines if the specified point is contained within the rectangular region defined by this - /// . - /// - /// The x-coordinate of the point to test. - /// The y-coordinate of the point to test. - /// This method returns if the point defined by and - /// is contained within this structure; otherwise . - public readonly bool Contains(int x, int y) => X <= x && x < X + Width && Y <= y && y < Y + Height; - - /// - /// Determines if the specified point is contained within the rectangular region defined by this - /// . - /// - /// The to test. - /// This method returns if the point represented by - /// is contained within this structure; otherwise . - public readonly bool Contains(Cords pt) => Contains(pt.X, pt.Y); - - /// - /// Determines if the rectangular region represented by is entirely contained within the - /// rectangular region represented by this . - /// - /// The to test. - /// This method returns if the rectangular region represented by - /// is entirely contained within this structure; otherwise . - public readonly bool Contains(Area rect) => - (X <= rect.X) && (rect.X + rect.Width <= X + Width) && - (Y <= rect.Y) && (rect.Y + rect.Height <= Y + Height); - - /// - /// Returns the hash code for this structure. - /// - /// An integer that represents the hash code for this rectangle. - public override readonly int GetHashCode() => -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - HashCode.Combine(X, Y, Width, Height); -#else - X ^ Y ^ Width ^ Height; -#endif - - /// - /// Inflates this by the specified amount. - /// - /// The amount to inflate this horizontally. - /// The amount to inflate this vertically. - public void Inflate(int width, int height) - { - unchecked - { - X -= width; - Y -= height; - - Width += 2 * width; - Height += 2 * height; - } - } - -#if HAS_DRAWING - /// - /// Inflates this by the specified amount. - /// - /// The amount to inflate this rectangle. - public void Inflate(System.Drawing.Size size) => Inflate(size.Width, size.Height); -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER - /// - /// Inflates this by the specified amount. - /// - /// The amount to inflate this rectangle. - public void Inflate(Windows.Foundation.Size size) => Inflate(unchecked((int)size.Width), unchecked((int)size.Height)); -#endif - - /// - /// Creates a that is inflated by the specified amount. - /// - /// The with which to start. This rectangle is not modified. - /// The amount to inflate this horizontally. - /// The amount to inflate this vertically. - public static Area Inflate(Area rect, int x, int y) - { - Area r = rect; - r.Inflate(x, y); - return r; - } - - /// - /// Creates a Area that represents the intersection between this Area and rect. - /// - /// The with which to intersect. - public void Intersect(Area rect) - { - Area result = Intersect(rect, this); - - X = result.X; - Y = result.Y; - Width = result.Width; - Height = result.Height; - } - - /// - /// Creates a rectangle that represents the intersection between a and b. If there is no intersection, an - /// empty rectangle is returned. - /// - /// A rectangle to intersect. - /// A rectangle to intersect. - /// A that represents the intersection of and . - public static Area Intersect(Area a, Area b) - { - int x1 = Math.Max(a.X, b.X); - int x2 = Math.Min(a.X + a.Width, b.X + b.Width); - int y1 = Math.Max(a.Y, b.Y); - int y2 = Math.Min(a.Y + a.Height, b.Y + b.Height); - - return x2 >= x1 && y2 >= y1 ? new Area(x1, y1, x2 - x1, y2 - y1) : Empty; - } - - /// - /// Determines if this rectangle intersects with rect. - /// - /// The rectangle to test. - /// This method returns if there is any intersection, otherwise . - public readonly bool IntersectsWith(Area rect) => - (rect.X < X + Width) && (X < rect.X + rect.Width) && - (rect.Y < Y + Height) && (Y < rect.Y + rect.Height); - - /// - /// Creates a rectangle that represents the union between a and b. - /// - /// A rectangle to union. - /// A rectangle to union. - /// A structure that bounds the union of the two structures. - public static Area Union(Area a, Area b) - { - int x1 = Math.Min(a.X, b.X); - int x2 = Math.Max(a.X + a.Width, b.X + b.Width); - int y1 = Math.Min(a.Y, b.Y); - int y2 = Math.Max(a.Y + a.Height, b.Y + b.Height); - - return new Area(x1, y1, x2 - x1, y2 - y1); - } - - /// - /// Adjusts the location of this rectangle by the specified amount. - /// - /// Amount to offset the location. - public void Offset(Cords pos) => Offset(pos.X, pos.Y); - - /// - /// Adjusts the location of this rectangle by the specified amount. - /// - /// The horizontal offset. - /// The vertical offset. - public void Offset(int x, int y) - { - unchecked - { - X += x; - Y += y; - } - } - - /// - /// Converts the attributes of this to a human readable string. - /// - /// A string that contains the position, width, and height of this structure ¾ - /// for example, {X=20, Y=20, Width=100, Height=50}. - public override readonly string ToString() => $"{{X={X},Y={Y},Width={Width},Height={Height}}}"; - } -} diff --git a/AdvancedSharpAdbClient/Models/ColorData.cs b/AdvancedSharpAdbClient/Models/ColorData.cs index 76e14e4b..d2144708 100644 --- a/AdvancedSharpAdbClient/Models/ColorData.cs +++ b/AdvancedSharpAdbClient/Models/ColorData.cs @@ -2,7 +2,12 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AdvancedSharpAdbClient.Models { /// /// Contains information about a color element. Information about each pixel on the screen @@ -11,29 +16,76 @@ namespace AdvancedSharpAdbClient /// For example, in a 24-bit RGB structure, the first byte contains the red color, /// the next byte the green color and the last byte the blue color. /// - public struct ColorData + /// The offset, in bits, within the byte array for a pixel, at which the + /// bytes that contain information for this color are stored. + /// The number of bits that contain information for this color. +#if HAS_BUFFERS + [CollectionBuilder(typeof(EnumerableBuilder), nameof(EnumerableBuilder.ColorDataCreate))] +#endif + public readonly record struct ColorData(uint Offset, uint Length) : IReadOnlyList { + /// + /// Gets or sets the offset, in bits, within the byte array for a pixel, at which the + /// bytes that contain information for this color are stored. + /// + public uint Offset { get; init; } = Offset; + /// /// Gets or sets the number of bits that contain information for this color. /// - public uint Length { get; set; } + public uint Length { get; init; } = Length; /// - /// Gets or sets the offset, in bits, within the byte array for a pixel, at which the - /// bytes that contain information for this color are stored. + /// The length of in bytes. /// - public uint Offset { get; set; } + public readonly int Count => 8; + + /// + public readonly byte this[int index] => + index < 0 || index >= Count + ? throw new IndexOutOfRangeException("Index was out of range. Must be non-negative and less than the size of the collection.") + : index switch + { + 0 => (byte)Offset, + 1 => (byte)(Offset >> 8), + 2 => (byte)(Offset >> 16), + 3 => (byte)(Offset >> 24), + + 4 => (byte)Length, + 5 => (byte)(Length >> 8), + 6 => (byte)(Length >> 16), + 7 => (byte)(Length >> 24), + + _ => throw new IndexOutOfRangeException("Index was out of range. Must be non-negative and less than the size of the collection.") + }; /// - /// Deconstruct the struct. + /// Deconstruct the struct. /// - /// The number of bits that contain information for this color. /// The offset, in bits, within the byte array for a pixel, at which the /// bytes that contain information for this color are stored. - public readonly void Deconstruct(out uint length, out uint offset) + /// The number of bits that contain information for this color. + public readonly void Deconstruct(out uint offset, out uint length) { - length = Length; offset = Offset; + length = Length; } + + /// + public readonly IEnumerator GetEnumerator() + { + yield return (byte)Offset; + yield return (byte)(Offset >> 8); + yield return (byte)(Offset >> 16); + yield return (byte)(Offset >> 24); + + yield return (byte)Length; + yield return (byte)(Length >> 8); + yield return (byte)(Length >> 16); + yield return (byte)(Length >> 24); + } + + /// + readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/AdvancedSharpAdbClient/Models/Cords.cs b/AdvancedSharpAdbClient/Models/Cords.cs deleted file mode 100644 index 14b0869a..00000000 --- a/AdvancedSharpAdbClient/Models/Cords.cs +++ /dev/null @@ -1,371 +0,0 @@ -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using System; - -namespace AdvancedSharpAdbClient -{ - /// - /// Represents an ordered pair of integer x- and y-coordinates that defines a point in a two-dimensional plane. - /// - public struct Cords : IEquatable - { - /// - /// Creates a new instance of the class with member data left uninitialized. - /// - public static readonly Cords Empty; - - /// - /// Initializes a new instance of the class. - /// - /// The horizontal "X" coordinate. - /// The vertical "Y" coordinate. - public Cords(int cx, int cy) - { - X = cx; - Y = cy; - } - - /// - /// Initializes a new instance of the class using coordinates specified by an integer value. - /// - public Cords(int dw) - { - X = LowInt16(dw); - Y = HighInt16(dw); - } - -#if HAS_DRAWING - /// - /// Initializes a new instance of the class from a . - /// - public Cords(System.Drawing.Point sz) - { - X = sz.X; - Y = sz.Y; - } - - /// - /// Initializes a new instance of the class from a . - /// - public Cords(System.Drawing.Size sz) - { - X = sz.Width; - Y = sz.Height; - } -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER -#pragma warning disable CS0419 // cref 特性中有不明确的引用 - /// - /// Initializes a new instance of the class from a . - /// - public Cords(Windows.Foundation.Point sz) - { - X = unchecked((int)sz.X); - Y = unchecked((int)sz.Y); - } - - /// - /// Initializes a new instance of the class from a . - /// - public Cords(Windows.Foundation.Size sz) - { - X = unchecked((int)sz.Width); - Y = unchecked((int)sz.Height); - } -#pragma warning restore CS0419 // cref 特性中有不明确的引用 -#endif - - /// - /// Gets a value indicating whether this is empty. - /// - public readonly bool IsEmpty => X == 0 && Y == 0; - - /// - /// Gets or sets the horizontal "X" coordinate. - /// - public int X { readonly get; set; } - - /// - /// Gets or sets the vertical "Y" coordinate. - /// - public int Y { readonly get; set; } - -#if HAS_DRAWING - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator System.Drawing.Point(Cords p) => new(p.X, p.Y); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator System.Drawing.PointF(Cords p) => new(p.X, p.Y); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static explicit operator System.Drawing.Size(Cords p) => new(p.X, p.Y); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator Cords(System.Drawing.Point p) => new(p); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static explicit operator Cords(System.Drawing.Size p) => new(p); - - /// - /// Translates a by a given . - /// - /// The to translate. - /// A that specifies the pair of numbers - /// to add to the coordinates of . - /// The translated . - public static Cords operator +(Cords pt, System.Drawing.Size sz) => Add(pt, sz); - - /// - /// Translates a by the negative of a given . - /// - /// The to translate. - /// A that specifies the pair of numbers - /// to subtract from the coordinates of . - /// A structure that is translated by the negative of a given structure. - public static Cords operator -(Cords pt, System.Drawing.Size sz) => Subtract(pt, sz); -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER -#pragma warning disable CS0419 // cref 特性中有不明确的引用 - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static implicit operator Windows.Foundation.Point(Cords p) => new(p.X, p.Y); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static explicit operator Windows.Foundation.Size(Cords p) => new(p.X, p.Y); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static explicit operator Cords(Windows.Foundation.Point p) => new(p); - - /// - /// Creates a with the coordinates of the specified . - /// - /// The to convert. - /// The that results from the conversion. - public static explicit operator Cords(Windows.Foundation.Size p) => new(p); - - /// - /// Translates a by a given . - /// - /// The to translate. - /// A that specifies the pair of numbers - /// to add to the coordinates of . - /// The translated . - public static Cords operator +(Cords pt, Windows.Foundation.Size sz) => Add(pt, sz); - - /// - /// Translates a by the negative of a given . - /// - /// The to translate. - /// A that specifies the pair of numbers - /// to subtract from the coordinates of . - /// A structure that is translated by the negative of a given structure. - public static Cords operator -(Cords pt, Windows.Foundation.Size sz) => Subtract(pt, sz); -#pragma warning restore CS0419 // cref 特性中有不明确的引用 -#endif - - /// - /// Compares two objects. The result specifies whether the values of the - /// and properties of the two - /// objects are equal. - /// - /// A to compare. - /// A to compare. - /// if the and values - /// of and are equal; otherwise, . - public static bool operator ==(Cords left, Cords right) => left.X == right.X && left.Y == right.Y; - - /// - /// Compares two objects. The result specifies whether the values of the - /// or properties of the two - /// objects are unequal. - /// - /// A to compare. - /// A to compare. - /// if the values of either the or values - /// of and differ; otherwise, . - public static bool operator !=(Cords left, Cords right) => !(left == right); - -#if HAS_DRAWING - /// - /// Translates a by a given . - /// - /// The to add. - /// The to add. - /// The that is the result of the addition operation. - public static Cords Add(Cords pt, System.Drawing.Size sz) => new(unchecked(pt.X + sz.Width), unchecked(pt.Y + sz.Height)); - - /// - /// Translates a by the negative of a given . - /// - /// The to be subtracted from. - /// The to subtract from the Point. - /// The that is the result of the subtraction operation. - public static Cords Subtract(Cords pt, System.Drawing.Size sz) => new(unchecked(pt.X - sz.Width), unchecked(pt.Y - sz.Height)); - - /// - /// Converts a to a by performing a ceiling operation on all the coordinates. - /// - /// The to convert. - /// The this method converts to. - public static Cords Ceiling(System.Drawing.PointF value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y))); - - /// - /// Converts a to a by performing a truncate operation on all the coordinates. - /// - /// The to convert. - /// The this method converts to. - public static Cords Truncate(System.Drawing.PointF value) => new(unchecked((int)value.X), unchecked((int)value.Y)); - - /// - /// Converts a to a by performing a round operation on all the coordinates. - /// - /// The to convert. - /// The this method converts to. - public static Cords Round(System.Drawing.PointF value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y))); -#endif - -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER -#pragma warning disable CS0419 // cref 特性中有不明确的引用 - /// - /// Translates a by a given . - /// - /// The to add. - /// The to add. - /// The that is the result of the addition operation. - public static Cords Add(Cords pt, Windows.Foundation.Size sz) => new(unchecked((int)(pt.X + sz.Width)), unchecked((int)(pt.Y + sz.Height))); - - /// - /// Translates a by the negative of a given . - /// - /// The to be subtracted from. - /// The to subtract from the Point. - /// The that is the result of the subtraction operation. - public static Cords Subtract(Cords pt, Windows.Foundation.Size sz) => new(unchecked((int)(pt.X - sz.Width)), unchecked((int)(pt.Y - sz.Height))); - - /// - /// Converts a to a by performing a ceiling operation on all the coordinates. - /// - /// The to convert. - /// The this method converts to. - public static Cords Ceiling(Windows.Foundation.Point value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y))); - - /// - /// Converts a to a by performing a truncate operation on all the coordinates. - /// - /// The to convert. - /// The this method converts to. - public static Cords Truncate(Windows.Foundation.Point value) => new(unchecked((int)value.X), unchecked((int)value.Y)); - - /// - /// Converts a to a by performing a round operation on all the coordinates. - /// - /// The to convert. - /// The this method converts to. - public static Cords Round(Windows.Foundation.Point value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y))); -#pragma warning restore CS0419 // cref 特性中有不明确的引用 -#endif - - /// - /// Specifies whether this contains the same coordinates as the specified - /// . - /// - /// The to test for equality. - /// if is a and has the same coordinates as this point instance. - public override readonly bool Equals(object obj) => obj is Cords cords && Equals(cords); - - /// - /// Specifies whether this contains the same coordinates as the specified - /// . - /// - /// The point to test for equality. - /// if has the same coordinates as this point instance. - public readonly bool Equals(Cords other) => this == other; - - /// - /// Returns a hash code for this . - /// - /// An integer value that specifies a hash value for this . - public override readonly int GetHashCode() => -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - HashCode.Combine(X, Y); -#else - X ^ Y; -#endif - - /// - /// Translates this by the specified amount. - /// - /// The amount to offset the x-coordinate. - /// The amount to offset the y-coordinate. - public void Offset(int dx, int dy) - { - unchecked - { - X += dx; - Y += dy; - } - } - - /// - /// Translates this by the specified amount. - /// - /// The used offset this . - public void Offset(Cords p) => Offset(p.X, p.Y); - - /// - /// Converts this to a human readable string. - /// - /// A string that represents this . - public override readonly string ToString() => $"{{X={X},Y={Y}}}"; - - /// - /// Deconstruct the class. - /// - /// The horizontal "X" coordinate. - /// The vertical "Y" coordinate. - public readonly void Deconstruct(out int cx, out int cy) - { - cx = X; - cy = Y; - } - - private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff)); - - private static short LowInt16(int n) => unchecked((short)(n & 0xffff)); - } -} diff --git a/AdvancedSharpAdbClient/Models/DeviceData.EventArgs.cs b/AdvancedSharpAdbClient/Models/DeviceData.EventArgs.cs new file mode 100644 index 00000000..56581276 --- /dev/null +++ b/AdvancedSharpAdbClient/Models/DeviceData.EventArgs.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; +using System.Collections.Generic; + +namespace AdvancedSharpAdbClient.Models +{ + /// + /// The event arguments that are passed when a device event occurs. + /// + /// The device. + public class DeviceDataEventArgs(DeviceData device) : EventArgs + { + /// + /// Gets the device where the change occurred. + /// + /// The device where the change occurred. + public DeviceData Device { get; } = device; + } + + /// + /// The event arguments that are passed when a device event occurs. + /// + /// The list of device. + public class DeviceDataNotifyEventArgs(IEnumerable devices) : EventArgs + { + /// + /// Gets the list of device where the change occurred. + /// + /// The list of device where the change occurred. + public IEnumerable Devices { get; } = devices; + } + + /// + /// The event arguments that are passed when a device event occurs. + /// + /// The device. + /// The device after the reported change. + public class DeviceDataConnectEventArgs(DeviceData device, bool isConnect) : DeviceDataEventArgs(device) + { + /// + /// Gets the connect state of the device after the reported change. + /// + public bool IsConnect { get; } = isConnect; + } + + /// + /// The event arguments that are passed when a device event occurs. + /// + /// The device. + /// The state of the device after the reported change. + /// The state of the device before the reported change. + public class DeviceDataChangeEventArgs(DeviceData device, DeviceState newState, DeviceState oldState) : DeviceDataEventArgs(device) + { + /// + /// Gets the state of the device after the reported change. + /// + public DeviceState NewState { get; } = newState; + + /// + /// Gets the state of the device before the reported change. + /// + public DeviceState OldState { get; } = oldState; + } +} diff --git a/AdvancedSharpAdbClient/Models/DeviceData.cs b/AdvancedSharpAdbClient/Models/DeviceData.cs index 08c1d54b..6bced8e0 100644 --- a/AdvancedSharpAdbClient/Models/DeviceData.cs +++ b/AdvancedSharpAdbClient/Models/DeviceData.cs @@ -3,29 +3,61 @@ // using System; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Represents a device that is connected to the Android Debug Bridge. /// - public partial class DeviceData + public partial class DeviceData : IEquatable { /// /// A regular expression that can be used to parse the device information that is returned by the Android Debut Bridge. /// - internal const string DeviceDataRegexString = @"^(?[a-zA-Z0-9_-]+(?:\s?[\.a-zA-Z0-9_-]+)?(?:\:\d{1,})?)\s+(?device|connecting|offline|unknown|bootloader|recovery|download|authorizing|unauthorized|host|no permissions)(?.*?)(\s+usb:(?[^:]+))?(?:\s+product:(?[^:]+))?(\s+model\:(?[\S]+))?(\s+device\:(?[\S]+))?(\s+features:(?[^:]+))?(\s+transport_id:(?[^:]+))?$"; + private const string DeviceDataRegexString = @"^(?[a-zA-Z0-9_-]+(?:\s?[\.a-zA-Z0-9_-]+)?(?:\:\d{1,})?)\s+(?device|connecting|offline|unknown|bootloader|recovery|download|authorizing|unauthorized|host|no permissions)(?.*?)(\s+usb:(?[^:]+))?(?:\s+product:(?[^:]+))?(\s+model\:(?[\S]+))?(\s+device\:(?[\S]+))?(\s+features:(?[^:]+))?(\s+transport_id:(?[^:]+))?$"; /// /// A regular expression that can be used to parse the device information that is returned by the Android Debut Bridge. /// private static readonly Regex Regex = DeviceDataRegex(); + /// + /// Initializes a new instance of the class. + /// + public DeviceData() { } + + /// + /// Initializes a new instance of the class based on + /// data retrieved from the Android Debug Bridge. + /// + /// The data retrieved from the Android Debug Bridge that represents a device. + public DeviceData(string data) + { + Match match = Regex.Match(data); + if (match.Success) + { + Serial = match.Groups["serial"].Value; + State = GetStateFromString(match.Groups["state"].Value); + Model = match.Groups["model"].Value; + Product = match.Groups["product"].Value; + Name = match.Groups["device"].Value; + Features = match.Groups["features"].Value; + Usb = match.Groups["usb"].Value; + TransportId = match.Groups["transport_id"].Value; + Message = match.Groups["message"].Value; + } + else + { + throw new ArgumentException($"Invalid device list data '{data}'"); + } + } + /// /// Gets or sets the device serial number. /// - public string Serial { get; set; } + public string Serial { get; init; } = string.Empty; /// /// Gets or sets the device state. @@ -35,37 +67,37 @@ public partial class DeviceData /// /// Gets or sets the device model name. /// - public string Model { get; set; } + public string Model { get; init; } = string.Empty; /// /// Gets or sets the device product name. /// - public string Product { get; set; } + public string Product { get; init; } = string.Empty; /// /// Gets or sets the device name. /// - public string Name { get; set; } + public string Name { get; init; } = string.Empty; /// /// Gets or sets the features available on the device. /// - public string Features { get; set; } + public string Features { get; init; } = string.Empty; /// /// Gets or sets the USB port to which this device is connected. Usually available on Linux only. /// - public string Usb { get; set; } + public string Usb { get; init; } = string.Empty; /// /// Gets or sets the transport ID for this device. /// - public string TransportId { get; set; } + public string TransportId { get; init; } = string.Empty; /// /// Gets or sets the device info message. Currently only seen for NoPermissions state. /// - public string Message { get; set; } + public string Message { get; init; } = string.Empty; /// /// Creates a new instance of the class based on @@ -73,23 +105,38 @@ public partial class DeviceData /// /// The data retrieved from the Android Debug Bridge that represents a device. /// A object that represents the device. - public static DeviceData CreateFromAdbData(string data) + public static DeviceData CreateFromAdbData(string data) => new(data); + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as DeviceData); + + /// + public bool Equals([NotNullWhen(true)] DeviceData? other) => + other is not null + && Serial == other.Serial + && State == other.State + && Model == other.Model + && Product == other.Product + && Name == other.Name + && Features == other.Features + && Usb == other.Usb + && TransportId == other.TransportId + && Message == other.Message; + + /// + public override int GetHashCode() { - Match m = Regex.Match(data); - return m.Success - ? new DeviceData() - { - Serial = m.Groups["serial"].Value, - State = GetStateFromString(m.Groups["state"].Value), - Model = m.Groups["model"].Value, - Product = m.Groups["product"].Value, - Name = m.Groups["device"].Value, - Features = m.Groups["features"].Value, - Usb = m.Groups["usb"].Value, - TransportId = m.Groups["transport_id"].Value, - Message = m.Groups["message"].Value - } - : throw new ArgumentException($"Invalid device list data '{data}'"); + HashCode hash = new(); + hash.Add(Serial); + hash.Add(State); + hash.Add(Model); + hash.Add(Product); + hash.Add(Name); + hash.Add(Features); + hash.Add(Usb); + hash.Add(TransportId); + hash.Add(Message); + return hash.ToHashCode(); } /// @@ -100,7 +147,7 @@ public static DeviceData CreateFromAdbData(string data) /// /// The device state string. /// The device state. - internal static DeviceState GetStateFromString(string state) + public static DeviceState GetStateFromString(string state) { // Default to the unknown state DeviceState value; @@ -118,7 +165,7 @@ internal static DeviceState GetStateFromString(string state) else { // Else, we try to match a value of the DeviceState enumeration. - if (!Utilities.TryParse(state, true, out value)) + if (!EnumExtensions.TryParse(state, true, out value)) { value = DeviceState.Unknown; } diff --git a/AdvancedSharpAdbClient/Models/DeviceDataEventArgs.cs b/AdvancedSharpAdbClient/Models/DeviceDataEventArgs.cs deleted file mode 100644 index d4d74ab9..00000000 --- a/AdvancedSharpAdbClient/Models/DeviceDataEventArgs.cs +++ /dev/null @@ -1,91 +0,0 @@ -// -// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. -// - -using System; -using System.Collections.Generic; - -namespace AdvancedSharpAdbClient -{ - /// - /// The event arguments that are passed when a device event occurs. - /// - public class DeviceDataEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The device. - public DeviceDataEventArgs(DeviceData device) => Device = device; - - /// - /// Gets the device where the change occurred. - /// - /// The device where the change occurred. - public DeviceData Device { get; } - } - - /// - /// The event arguments that are passed when a device event occurs. - /// - public class DeviceDataNotifyEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The list of device. - public DeviceDataNotifyEventArgs(IEnumerable devices) => Devices = devices; - - /// - /// Gets the list of device where the change occurred. - /// - /// The list of device where the change occurred. - public IEnumerable Devices { get; } - } - - /// - /// The event arguments that are passed when a device event occurs. - /// - public class DeviceDataConnectEventArgs : DeviceDataEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The device. - /// The device after the reported change. - public DeviceDataConnectEventArgs(DeviceData device, bool isConnect) : base(device) => IsConnect = isConnect; - - /// - /// Gets the connect state of the device after the reported change. - /// - public bool IsConnect { get; } - } - - /// - /// The event arguments that are passed when a device event occurs. - /// - public class DeviceDataChangeEventArgs : DeviceDataEventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// The device. - /// The state of the device after the reported change. - /// The state of the device before the reported change. - public DeviceDataChangeEventArgs(DeviceData device, DeviceState newState, DeviceState oldState) : base(device) - { - NewState = newState; - OldState = oldState; - } - - /// - /// Gets the state of the device after the reported change. - /// - public DeviceState NewState { get; } - - /// - /// Gets the state of the device before the reported change. - /// - public DeviceState OldState { get; } - } -} diff --git a/AdvancedSharpAdbClient/Models/Element.cs b/AdvancedSharpAdbClient/Models/Element.cs index 315a13e6..454a89a5 100644 --- a/AdvancedSharpAdbClient/Models/Element.cs +++ b/AdvancedSharpAdbClient/Models/Element.cs @@ -2,236 +2,228 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Xml; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Implement of screen element, likes Selenium. /// - public class Element + public class Element : IEquatable { - /// - /// Gets or sets the current ADB client that manages the connection. - /// - protected IAdbClient Client { get; set; } - - /// - /// Gets the current device containing the element. - /// - protected DeviceData Device { get; } - - /// - /// Gets the coordinates and size of the element. - /// - public Area Area { get; } - - /// - /// Gets or sets the coordinates of the element to click. Default is the center of area. - /// - public Cords Cords { get; set; } - - /// - /// Gets the children of this element. - /// - public List Children { get; } - - /// - /// Gets the element attributes. - /// - public Dictionary Attributes { get; } - - /// - /// Gets the of this element. - /// - public XmlNode Node { get; } - - /// - /// Gets or sets the element at the specified index. - /// - /// The zero-based index of the element to get or set. - /// The element at the specified index. - public Element this[int index] => Children[index]; - - /// - /// Initializes a new instance of the class. - /// - /// The current ADB client that manages the connection. - /// The current device containing the element. - /// The coordinates of the element to click. - /// Gets or sets element attributes. - public Element(IAdbClient client, DeviceData device, Cords cords, Dictionary attributes) - { - Client = client; - Device = device; - Cords = cords; - Attributes = attributes; - } - - /// - /// Initializes a new instance of the class. - /// - /// The current ADB client that manages the connection. - /// The current device containing the element. - /// The coordinates and size of the element. - /// Gets or sets element attributes. - public Element(IAdbClient client, DeviceData device, Area area, Dictionary attributes) - { - Client = client; - Device = device; - Area = area; - Attributes = attributes; - Cords = area.Center; // Average x1, y1, x2, y2 - } + private static readonly char[] separator = ['[', ']', ',', ' ']; /// /// Initializes a new instance of the class. /// /// The current ADB client that manages the connection. /// The current device containing the element. - /// The of the element. - /// The children of the element. - /// The coordinates and size of the element. + /// The coordinates and size of the element. /// Gets or sets element attributes. - public Element(IAdbClient client, DeviceData device, XmlNode node, List children, Area area, Dictionary attributes) + public Element(IAdbClient client, DeviceData device, Rectangle rectangle, Dictionary? attributes = null) { Client = client; Device = device; - Node = node; - Children = children; - Area = area; + Bounds = rectangle; Attributes = attributes; - Cords = area.Center; // Average x1, y1, x2, y2 } -#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER /// /// Initializes a new instance of the class. /// /// The current ADB client that manages the connection. /// The current device containing the element. - /// The of the element. - /// The children of the element. - /// The coordinates and size of the element. - /// Gets or sets element attributes. - public Element(IAdbClient client, DeviceData device, Windows.Data.Xml.Dom.IXmlNode node, List children, Area area, Dictionary attributes) + /// The of the element. + public Element(IAdbClient client, DeviceData device, XmlNode? xmlNode) { - XmlDocument doc = new(); - doc.LoadXml(node.GetXml()); - Client = client; Device = device; - Node = doc.FirstChild; - Children = children; - Area = area; - Attributes = attributes; - Cords = area.Center; // Average x1, y1, x2, y2 - } -#endif + Node = xmlNode; - /// - /// Creates a new with the specified . - /// - /// The current ADB client that manages the connection. - /// The current device containing the element. - /// The of the element. - /// The new that this method creates. - public static Element FromXmlNode(IAdbClient client, DeviceData device, XmlNode xmlNode) - { - string bounds = xmlNode.Attributes["bounds"].Value; - if (bounds != null) + if (xmlNode?.Attributes != null) { - int[] cords = bounds.Replace("][", ",").Replace("[", "").Replace("]", "").Split(',').Select(int.Parse).ToArray(); // x1, y1, x2, y2 - Dictionary attributes = new(xmlNode.Attributes.Count); - foreach (XmlAttribute at in xmlNode.Attributes) + if (xmlNode.Attributes["bounds"]?.Value is string bounds) { - attributes[at.Name] = at.Value; + string[] cords = bounds.Split(separator, StringSplitOptions.RemoveEmptyEntries); // x1, y1, x2, y2 + Bounds = Rectangle.FromLTRB(int.Parse(cords[0]), int.Parse(cords[1]), int.Parse(cords[2]), int.Parse(cords[3])); } - Area area = Area.FromLTRB(cords[0], cords[1], cords[2], cords[3]); - XmlNodeList childNodes = xmlNode.ChildNodes; - List elements = new(childNodes?.Count ?? 0); + + Attributes = new(xmlNode.Attributes.Count); + foreach (XmlAttribute at in xmlNode.Attributes.OfType()) + { + Attributes[at.Name] = at.Value; + } + } + + static IEnumerable FindElements(IAdbClient client, DeviceData device, XmlNode? xmlNode) + { + XmlNodeList? childNodes = xmlNode?.ChildNodes; if (childNodes != null) { - for (int i = 0; i < childNodes.Count; i++) + for (int i = 0; i < childNodes!.Count; i++) { - Element element = FromXmlNode(client, device, childNodes[i]); + Element? element = FromXmlNode(client, device, childNodes?[i]); if (element != null) { - elements.Add(element); + yield return element; } } } - return new Element(client, device, xmlNode, elements, area, attributes); } - return null; + Children = FindElements(client, device, xmlNode); } #if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER /// - /// Creates a new with the specified . + /// Initializes a new instance of the class. /// /// The current ADB client that manages the connection. /// The current device containing the element. /// The of the element. - /// The new that this method creates. - public static Element FromIXmlNode(IAdbClient client, DeviceData device, Windows.Data.Xml.Dom.IXmlNode xmlNode) + public Element(IAdbClient client, DeviceData device, Windows.Data.Xml.Dom.IXmlNode xmlNode) { - string bounds = xmlNode.Attributes?.GetNamedItem("bounds")?.NodeValue?.ToString(); - if (bounds != null) + Client = client; + Device = device; + + ExceptionExtensions.ThrowIfNull(xmlNode); + XmlDocument doc = new(); + doc.LoadXml(xmlNode.GetXml()); + Node = doc.FirstChild; + + if (xmlNode.Attributes != null) { - int[] cords = bounds.Replace("][", ",").Replace("[", "").Replace("]", "").Split(',').Select(int.Parse).ToArray(); // x1, y1, x2, y2 - Dictionary attributes = new(xmlNode.Attributes.Count); + if (xmlNode.Attributes.GetNamedItem("bounds")?.NodeValue?.ToString() is string bounds) + { + string[] cords = bounds.Split(separator, StringSplitOptions.RemoveEmptyEntries); // x1, y1, x2, y2 + Bounds = Rectangle.FromLTRB(int.Parse(cords[0]), int.Parse(cords[1]), int.Parse(cords[2]), int.Parse(cords[3])); + } + + Attributes = new(xmlNode.Attributes.Count); foreach (Windows.Data.Xml.Dom.IXmlNode at in xmlNode.Attributes) { - attributes[at.NodeName] = at.NodeValue.ToString(); + Attributes[at.NodeName] = at.NodeValue.ToString(); } - Area area = Area.FromLTRB(cords[0], cords[1], cords[2], cords[3]); + } + + static IEnumerable FindElements(IAdbClient client, DeviceData device, Windows.Data.Xml.Dom.IXmlNode xmlNode) + { Windows.Data.Xml.Dom.XmlNodeList childNodes = xmlNode.ChildNodes; - List elements = new(childNodes?.Count ?? 0); if (childNodes != null) { foreach (Windows.Data.Xml.Dom.IXmlNode childNode in childNodes) { - Element element = FromIXmlNode(client, device, childNode); + Element? element = FromIXmlNode(client, device, childNode); if (element != null) { - elements.Add(element); + yield return element; } } } - return new Element(client, device, xmlNode, elements, area, attributes); } - return null; + Children = FindElements(client, device, xmlNode); } #endif /// - /// Gets the count of in this element. + /// Gets or sets the current ADB client that manages the connection. /// - public virtual int GetChildCount() - { - int count = Children.Count; - Children.ForEach(x => count += x.GetChildCount()); - return count; - } + public IAdbClient Client { get; init; } /// - /// Clicks on this coordinates. + /// Gets the current device containing the element. /// - public void Click() => Client.Click(Device, Cords); + public DeviceData Device { get; init; } -#if HAS_TASK /// - /// Clicks on this coordinates. + /// Gets the coordinates and size of the element. /// - /// A which can be used to cancel the asynchronous operation. - public async Task ClickAsync(CancellationToken cancellationToken = default) => - await Client.ClickAsync(Device, Cords, cancellationToken); + public Rectangle Bounds { get; init; } + + /// + /// Gets the children of this element. + /// + public IEnumerable? Children { get; init; } + + /// + /// Gets the element attributes. + /// + public Dictionary? Attributes { get; init; } + + /// + /// Gets the of this element. + /// + public XmlNode? Node { get; init; } + + /// + /// Gets the coordinates of the the center of the element. + /// + public Point Center => unchecked(new(Bounds.X + (Bounds.Width / 2), Bounds.Y + (Bounds.Height / 2))); + + /// + /// Gets the text of the element. + /// + public string? Text => Attributes?.TryGetValue("text", out string? text) == true ? text : string.Empty; + + /// + /// Gets the class name of the element. + /// + public string? Class => Attributes?.TryGetValue("class", out string? @class) == true ? @class : string.Empty; + + /// + /// Gets the package name of the element. + /// + public string? Package => Attributes?.TryGetValue("package", out string? package) == true ? package : string.Empty; + + /// + /// Gets the resource ID of the element. + /// + public string? ResourceID => Attributes?.TryGetValue("resource-id", out string? resource_id) == true ? resource_id : string.Empty; + + /// + /// Gets the element at the specified index. + /// + /// The zero-based index of the element to get or set. + /// The element at the specified index. + /// The index method is index by . + public Element? this[int index] => Children?.ElementAt(index); + + /// + /// Creates a new with the specified . + /// + /// The current ADB client that manages the connection. + /// The current device containing the element. + /// The of the element. + /// The new that this method creates. + public static Element? FromXmlNode(IAdbClient client, DeviceData device, XmlNode? xmlNode) => + xmlNode?.Attributes?["bounds"] != null ? new Element(client, device, xmlNode) : null; + +#if WINDOWS_UWP || WINDOWS10_0_17763_0_OR_GREATER + /// + /// Creates a new with the specified . + /// + /// The current ADB client that manages the connection. + /// The current device containing the element. + /// The of the element. + /// The new that this method creates. + public static Element? FromIXmlNode(IAdbClient client, DeviceData device, Windows.Data.Xml.Dom.IXmlNode xmlNode) => + xmlNode.Attributes?.GetNamedItem("bounds") != null ? new Element(client, device, xmlNode) : null; #endif + /// + /// Gets the count of in this element. + /// + public virtual int GetChildCount() => Children == null ? 0 : Children.Count() + Children.Select(x => x.GetChildCount()).Sum(); + + /// + /// Clicks on this coordinates. + /// + public void Click() => Client.Click(Device, Center); + /// /// Send text to device. Doesn't support Russian. /// @@ -242,7 +234,30 @@ public void SendText(string text) Client.SendText(Device, text); } + /// + /// Clear the input text. Use if the element is focused. + /// + [MemberNotNull(nameof(Text))] + public void ClearInput() => ClearInput(Text!.Length); + + /// + /// Clear the input text. Use if the element is focused. + /// + /// The length of text to clear. + public void ClearInput(int charCount) + { + Click(); // focuses + Client.ClearInput(Device, charCount); + } + #if HAS_TASK + /// + /// Clicks on this coordinates. + /// + /// A which can be used to cancel the asynchronous operation. + public Task ClickAsync(CancellationToken cancellationToken = default) => + Client.ClickAsync(Device, Center, cancellationToken); + /// /// Send text to device. Doesn't support Russian. /// @@ -251,47 +266,113 @@ public void SendText(string text) /// A which represents the asynchronous operation. public async Task SendTextAsync(string text, CancellationToken cancellationToken = default) { - await ClickAsync(cancellationToken); - await Client.SendTextAsync(Device, text, cancellationToken); + await ClickAsync(cancellationToken).ConfigureAwait(false); + await Client.SendTextAsync(Device, text, cancellationToken).ConfigureAwait(false); } -#endif /// - /// Clear the input text. Use if the element is focused. + /// Clear the input text. Use if the element is focused. /// - /// The length of text to clear. - public void ClearInput(int charCount = 0) - { - Click(); // focuses - if (charCount == 0) - { - Client.ClearInput(Device, Attributes["text"].Length); - } - else - { - Client.ClearInput(Device, charCount); - } - } + /// A which can be used to cancel the asynchronous operation. + /// A which represents the asynchronous operation. + [MemberNotNull(nameof(Text))] + public Task ClearInputAsync(CancellationToken cancellationToken = default) => + ClearInputAsync(Text!.Length, cancellationToken); -#if HAS_TASK /// - /// Clear the input text. Use if the element is focused. + /// Clear the input text. Use if the element is focused. /// /// The length of text to clear. /// A which can be used to cancel the asynchronous operation. /// A which represents the asynchronous operation. - public async Task ClearInputAsync(int charCount = 0, CancellationToken cancellationToken = default) + public async Task ClearInputAsync(int charCount, CancellationToken cancellationToken = default) { - await ClickAsync(cancellationToken); // focuses - if (charCount == 0) + await ClickAsync(cancellationToken).ConfigureAwait(false); // focuses + await Client.ClearInputAsync(Device, charCount, cancellationToken).ConfigureAwait(false); + } +#endif + + /// + /// Find the first descendant element matching a given predicate, using a depth-first search. + /// + /// The predicate to use to match the descendant nodes. + /// The descendant that was found, or . + public Element? FindDescendant(Func predicate) + { + if (Children == null) { return null; } + + foreach (Element child in Children) { - await Client.ClearInputAsync(Device, Attributes["text"].Length, cancellationToken); + if (predicate(child)) + { + return child; + } + + Element? descendant = child.FindDescendant(predicate); + + if (descendant != null) + { + return descendant; + } } - else + + return null; + } + + /// + /// Find the first descendant (or self) element matching a given predicate, using a depth-first search. + /// + /// The predicate to use to match the descendant nodes. + /// The descendant (or self) that was found, or . + public Element? FindDescendantOrSelf(Func predicate) => + predicate(this) ? this : FindDescendant(predicate); + + /// + /// Find all descendant elements of the specified element. This method can be chained with + /// LINQ calls to add additional filters or projections on top of the returned results. + /// + /// This method is meant to provide extra flexibility in specific scenarios and it should not + /// be used when only the first item is being looked for. In those cases, use one of the + /// available overloads instead, which will + /// offer a more compact syntax as well as better performance in those cases. + /// + /// + /// All the descendant instance from . + public IEnumerable FindDescendants() + { + if (Children == null) { yield break; } + foreach (Element child in Children) { - await Client.ClearInputAsync(Device, charCount, cancellationToken); + yield return child; + + foreach (Element childOfChild in child.FindDescendants()) + { + yield return childOfChild; + } } } -#endif + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as Element); + + /// + public bool Equals([NotNullWhen(true)] Element? other) => + other is not null + && Client == other.Client + && Device == other.Device + && (Node == null + ? Bounds == other.Bounds + && Attributes == other.Attributes + : Node == other.Node); + + /// + public override int GetHashCode() => + Node == null + ? HashCode.Combine(Client, Device, Bounds, Attributes) + : HashCode.Combine(Client, Device, Node); + + /// + public override string? ToString() => + string.IsNullOrEmpty(Text) ? string.IsNullOrEmpty(Class) ? base.ToString() : Class : Text; } } diff --git a/AdvancedSharpAdbClient/Models/Enums/AppStatus.cs b/AdvancedSharpAdbClient/Models/Enums/AppStatus.cs index f69dad38..280d5122 100644 --- a/AdvancedSharpAdbClient/Models/Enums/AppStatus.cs +++ b/AdvancedSharpAdbClient/Models/Enums/AppStatus.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// The status of an application if it is stopped or running in foreground or background. diff --git a/AdvancedSharpAdbClient/Models/Enums/DeviceState.cs b/AdvancedSharpAdbClient/Models/Enums/DeviceState.cs index 2b44c453..d563ff6f 100644 --- a/AdvancedSharpAdbClient/Models/Enums/DeviceState.cs +++ b/AdvancedSharpAdbClient/Models/Enums/DeviceState.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Defines the state of an Android device connected to the Android Debug Bridge. diff --git a/AdvancedSharpAdbClient/Models/Enums/ForwardProtocol.cs b/AdvancedSharpAdbClient/Models/Enums/ForwardProtocol.cs index 27cc2b00..e2ebd3af 100644 --- a/AdvancedSharpAdbClient/Models/Enums/ForwardProtocol.cs +++ b/AdvancedSharpAdbClient/Models/Enums/ForwardProtocol.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Represents a protocol which is being forwarded over adb. diff --git a/AdvancedSharpAdbClient/Models/Enums/StartServerResult.cs b/AdvancedSharpAdbClient/Models/Enums/StartServerResult.cs index 31a92d51..1d4c2796 100644 --- a/AdvancedSharpAdbClient/Models/Enums/StartServerResult.cs +++ b/AdvancedSharpAdbClient/Models/Enums/StartServerResult.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Gives information about a operation. @@ -23,6 +23,11 @@ public enum StartServerResult : byte /// /// The adb server was not running, and a new instance of the adb server was started. /// - Started + Started, + + /// + /// An operation is already in progress. + /// + Starting } } diff --git a/AdvancedSharpAdbClient/Models/Enums/SyncCommand.cs b/AdvancedSharpAdbClient/Models/Enums/SyncCommand.cs index a5dc29bf..5d179772 100644 --- a/AdvancedSharpAdbClient/Models/Enums/SyncCommand.cs +++ b/AdvancedSharpAdbClient/Models/Enums/SyncCommand.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Defines a command that can be sent to, or a response that can be received from the sync service. @@ -12,7 +12,7 @@ public enum SyncCommand : byte /// /// List the files in a folder. /// - LIST, + LIST = 1, /// /// Retrieve a file from device. diff --git a/AdvancedSharpAdbClient/Models/Enums/TransportType.cs b/AdvancedSharpAdbClient/Models/Enums/TransportType.cs index 283ae50e..113938ad 100644 --- a/AdvancedSharpAdbClient/Models/Enums/TransportType.cs +++ b/AdvancedSharpAdbClient/Models/Enums/TransportType.cs @@ -2,7 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Specifies the transport type used between the device and the Android Debug Bridge server. diff --git a/AdvancedSharpAdbClient/Models/Enums/UnixFileMode.cs b/AdvancedSharpAdbClient/Models/Enums/UnixFileType.cs similarity index 88% rename from AdvancedSharpAdbClient/Models/Enums/UnixFileMode.cs rename to AdvancedSharpAdbClient/Models/Enums/UnixFileType.cs index 0aa3cbde..6190f701 100644 --- a/AdvancedSharpAdbClient/Models/Enums/UnixFileMode.cs +++ b/AdvancedSharpAdbClient/Models/Enums/UnixFileType.cs @@ -1,19 +1,19 @@ -// +// // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // using System; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Describes the properties of a file on an Android device. /// [Flags] - public enum UnixFileMode : ushort + public enum UnixFileType { /// - /// The mask that can be used to retrieve the file type from a . + /// The mask that can be used to retrieve the file type from a . /// TypeMask = 0x8000, diff --git a/AdvancedSharpAdbClient/Models/FileStatistics.cs b/AdvancedSharpAdbClient/Models/FileStatistics.cs index 6f6af668..f478c94a 100644 --- a/AdvancedSharpAdbClient/Models/FileStatistics.cs +++ b/AdvancedSharpAdbClient/Models/FileStatistics.cs @@ -3,38 +3,70 @@ // using System; +using System.Diagnostics.CodeAnalysis; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Contains information about a file on the remote device. /// - public class FileStatistics + public struct FileStatistics : IEquatable { + /// + /// Initializes a new instance of the struct. + /// + public FileStatistics() { } + /// /// Gets or sets the path of the file. /// - public string Path { get; set; } + public string Path { get; set; } = string.Empty; /// - /// Gets or sets the attributes of the file. + /// Gets or sets the attributes of the file. /// - public UnixFileMode FileMode { get; set; } + public UnixFileType FileType { get; init; } /// /// Gets or sets the total file size, in bytes. /// - public int Size { get; set; } + public int Size { get; init; } /// /// Gets or sets the time of last modification. /// - public DateTimeOffset Time { get; set; } + public DateTimeOffset Time { get; init; } + + /// + public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is FileStatistics other && Equals(other); + + /// + public readonly bool Equals(FileStatistics other) => + Path == other.Path + && FileType == other.FileType + && Size == other.Size + && Time == other.Time; + + /// + public override readonly int GetHashCode() => HashCode.Combine(Path, FileType, Size, Time); + + /// + public override readonly string ToString() => StringExtensions.Join("\t", FileType, Time, FileType, Path); + + /// + /// Tests whether two objects are equally. + /// + /// The structure that is to the left of the equality operator. + /// The structure that is to the right of the equality operator. + /// This operator returns if the two structures are equally; otherwise . + public static bool operator ==(FileStatistics left, FileStatistics right) => left.Equals(right); /// - /// Gets a that represents the current object. + /// Tests whether two objects are different. /// - /// The of the current object. - public override string ToString() => Path; + /// The structure that is to the left of the inequality operator. + /// The structure that is to the right of the inequality operator. + /// This operator returns if the two structures are unequally; otherwise . + public static bool operator !=(FileStatistics left, FileStatistics right) => !left.Equals(right); } } diff --git a/AdvancedSharpAdbClient/Models/ForwardData.cs b/AdvancedSharpAdbClient/Models/ForwardData.cs index 96b1d4f4..eb2e4093 100644 --- a/AdvancedSharpAdbClient/Models/ForwardData.cs +++ b/AdvancedSharpAdbClient/Models/ForwardData.cs @@ -2,72 +2,91 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AdvancedSharpAdbClient.Models { /// /// Contains information about port forwarding configured by the Android Debug Bridge. /// - public class ForwardData + public class ForwardData : IEquatable { /// - /// Gets or sets the serial number of the device for which the port forwarding is configured. + /// Initializes a new instance of the class. /// - public string SerialNumber { get; set; } + public ForwardData() { } /// - /// Gets or sets a that represents the local (PC) endpoint. + /// Initializes a new instance of the class. /// - public string Local { get; set; } + /// The serial number of the device for which the port forwarding is configured. + /// The that represents the local (PC) endpoint. + /// The that represents the remote (device) endpoint. + public ForwardData(string serialNumber, string local, string remote) + { + SerialNumber = serialNumber; + Local = local; + Remote = remote; + } /// - /// Gets a that represents the local (PC) endpoint. + /// Initializes a new instance of the class by parsing a . /// - public ForwardSpec LocalSpec => ForwardSpec.Parse(Local); + /// The value to parse. + public ForwardData(string value) + { + string[] parts = value.Split(' '); + SerialNumber = parts[0]; + Local = parts[1]; + Remote = parts[2]; + } /// - /// Gets or sets a that represents the remote (device) endpoint. + /// Gets or sets the serial number of the device for which the port forwarding is configured. /// - public string Remote { get; set; } + public string SerialNumber { get; init; } = string.Empty; /// - /// Gets a that represents the remote (device) endpoint. + /// Gets or sets a that represents the local (PC) endpoint. /// - public ForwardSpec RemoteSpec => ForwardSpec.Parse(Remote); + public string Local { get; init; } = string.Empty; /// - /// Initializes a new instance of the struct. + /// Gets a that represents the local (PC) endpoint. /// - public ForwardData() - { - } + public ForwardSpec LocalSpec => new(Local); /// - /// Initializes a new instance of the struct. + /// Gets or sets a that represents the remote (device) endpoint. /// - /// The serial number of the device for which the port forwarding is configured. - /// The that represents the local (PC) endpoint. - /// The that represents the remote (device) endpoint. - public ForwardData(string serialNumber, string local, string remote) - { - SerialNumber = serialNumber; - Local = local; - Remote = remote; - } + public string Remote { get; init; } = string.Empty; + + /// + /// Gets a that represents the remote (device) endpoint. + /// + public ForwardSpec RemoteSpec => new(Remote); /// /// Creates a new instance of the class by parsing a . /// /// The value to parse. /// A object that represents the port forwarding information contained in . - public static ForwardData FromString(string value) - { - if (value == null) - { - return null; - } - string[] parts = value.Split(' '); - return new ForwardData(parts[0], parts[1], parts[2]); - } + [return: NotNullIfNotNull(nameof(value))] + public static ForwardData? FromString(string? value) => value == null ? null : new ForwardData(value); + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as ForwardData); + + /// + public bool Equals([NotNullWhen(true)] ForwardData? other) => + other is not null + && SerialNumber == other.SerialNumber + && Local == other.Local + && Remote == other.Remote; + + /// + public override int GetHashCode() => HashCode.Combine(SerialNumber, Local, Remote); /// public override string ToString() => $"{SerialNumber} {Local} {Remote}"; diff --git a/AdvancedSharpAdbClient/Models/ForwardSpec.cs b/AdvancedSharpAdbClient/Models/ForwardSpec.cs index 305a69a6..f61ab3bf 100644 --- a/AdvancedSharpAdbClient/Models/ForwardSpec.cs +++ b/AdvancedSharpAdbClient/Models/ForwardSpec.cs @@ -2,24 +2,24 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Represents an adb forward specification as used by the various adb port forwarding functions. /// - public class ForwardSpec + public readonly struct ForwardSpec : IEquatable { /// /// Provides a mapping between a and a /// value, used for the representation of the /// class. /// - private static readonly Dictionary Mappings = new(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary Mappings = new(6, StringComparer.OrdinalIgnoreCase) { { "tcp", ForwardProtocol.Tcp }, { "localabstract", ForwardProtocol.LocalAbstract }, @@ -30,57 +30,32 @@ public class ForwardSpec }; /// - /// Gets or sets the protocol which is being forwarded. - /// - public ForwardProtocol Protocol { get; set; } - - /// - /// Gets or sets, when the is , the port - /// number of the port being forwarded. - /// - public int Port { get; set; } - - /// - /// Gets or sets, when the is , - /// or , - /// the Unix domain socket name of the socket being forwarded. - /// - public string SocketName { get; set; } - - /// - /// Gets or sets, when the is , - /// the process id of the process to which the debugger is attached. + /// Initializes a new instance of the struct. /// - public int ProcessId { get; set; } + public ForwardSpec() { } /// - /// Creates a from its representation. + /// Initializes a new instance of the struct from its representation. /// /// A which represents a . - /// A which represents . - public static ForwardSpec Parse(string spec) + public ForwardSpec(string spec) { ExceptionExtensions.ThrowIfNull(spec); - string[] parts = spec.Split(new char[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries); + string[] parts = spec.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) { throw new ArgumentOutOfRangeException(nameof(spec)); } - if (!Mappings.ContainsKey(parts[0])) + if (!Mappings.TryGetValue(parts[0], out ForwardProtocol value)) { throw new ArgumentOutOfRangeException(nameof(spec)); } - ForwardProtocol protocol = Mappings[parts[0]]; - - ForwardSpec value = new() - { - Protocol = protocol - }; - + ForwardProtocol protocol = value; + Protocol = protocol; bool isInt = int.TryParse(parts[1], out int intValue); @@ -91,8 +66,7 @@ public static ForwardSpec Parse(string spec) { throw new ArgumentOutOfRangeException(nameof(spec)); } - - value.ProcessId = intValue; + ProcessId = intValue; break; case ForwardProtocol.Tcp: @@ -100,27 +74,55 @@ public static ForwardSpec Parse(string spec) { throw new ArgumentOutOfRangeException(nameof(spec)); } - - value.Port = intValue; + Port = intValue; break; case ForwardProtocol.LocalAbstract or ForwardProtocol.LocalFilesystem or ForwardProtocol.LocalReserved or ForwardProtocol.Device: - value.SocketName = parts[1]; + SocketName = parts[1]; break; } - - return value; } + /// + /// Gets or sets the protocol which is being forwarded. + /// + public ForwardProtocol Protocol { get; init; } + + /// + /// Gets or sets, when the is , the port + /// number of the port being forwarded. + /// + public int Port { get; init; } + + /// + /// Gets or sets, when the is , + /// or , + /// the Unix domain socket name of the socket being forwarded. + /// + public string? SocketName { get; init; } + + /// + /// Gets or sets, when the is , + /// the process id of the process to which the debugger is attached. + /// + public int ProcessId { get; init; } + + /// + /// Creates a from its representation. + /// + /// A which represents a . + /// A which represents . + public static ForwardSpec Parse(string spec) => new(spec); + /// public override string ToString() { - string protocolString = Mappings.FirstOrDefault(v => v.Value == Protocol).Key; - - return Protocol switch + ForwardProtocol protocol = Protocol; + string protocolString = Mappings.FirstOrDefault(v => v.Value == protocol).Key; + return protocol switch { ForwardProtocol.JavaDebugWireProtocol => $"{protocolString}:{ProcessId}", ForwardProtocol.Tcp => $"{protocolString}:{Port}", @@ -133,27 +135,23 @@ or ForwardProtocol.LocalReserved } /// - public override int GetHashCode() => - (int)Protocol - ^ Port - ^ ProcessId - ^ (SocketName == null ? 1 : SocketName.GetHashCode()); + public override int GetHashCode() => HashCode.Combine(Protocol, Port, ProcessId, SocketName); /// - public override bool Equals(object obj) - { - return obj is ForwardSpec other - && other.Protocol == Protocol - && Protocol switch - { - ForwardProtocol.JavaDebugWireProtocol => ProcessId == other.ProcessId, - ForwardProtocol.Tcp => Port == other.Port, - ForwardProtocol.LocalAbstract - or ForwardProtocol.LocalFilesystem - or ForwardProtocol.LocalReserved - or ForwardProtocol.Device => string.Equals(SocketName, other.SocketName), - _ => false, - }; - } + public override bool Equals([NotNullWhen(true)] object? obj) => obj is ForwardSpec other && Equals(other); + + /// + public bool Equals(ForwardSpec other) => + other.Protocol == Protocol + && Protocol switch + { + ForwardProtocol.JavaDebugWireProtocol => ProcessId == other.ProcessId, + ForwardProtocol.Tcp => Port == other.Port, + ForwardProtocol.LocalAbstract + or ForwardProtocol.LocalFilesystem + or ForwardProtocol.LocalReserved + or ForwardProtocol.Device => string.Equals(SocketName, other.SocketName), + _ => false, + }; } } diff --git a/AdvancedSharpAdbClient/Models/Framebuffer.cs b/AdvancedSharpAdbClient/Models/Framebuffer.cs index 6093e46a..139fd1b6 100644 --- a/AdvancedSharpAdbClient/Models/Framebuffer.cs +++ b/AdvancedSharpAdbClient/Models/Framebuffer.cs @@ -2,69 +2,74 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Runtime.InteropServices; using System.Threading; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Provides access to the framebuffer (that is, a copy of the image being displayed on the device screen). /// - public class Framebuffer : IDisposable + /// The device for which to fetch the frame buffer. + /// The at which the adb server is listening. + /// The to create . + public class Framebuffer(DeviceData device, EndPoint endPoint, Func adbSocketFactory) : IDisposable { - private byte[] headerData; + private byte[] headerData = new byte[FramebufferHeader.MaxLength]; private bool headerInitialized; private bool disposed = false; + /// + /// The to create . + /// + protected readonly Func adbSocketFactory = adbSocketFactory; + /// /// Initializes a new instance of the class. /// /// The device for which to fetch the frame buffer. /// The at which the adb server is listening. - public Framebuffer(DeviceData device, EndPoint endPoint) - { - Device = device ?? throw new ArgumentNullException(nameof(device)); + public Framebuffer(DeviceData device, EndPoint endPoint) : this(device, endPoint, Factories.AdbSocketFactory) { } - EndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint)); + /// + /// Initializes a new instance of the class. + /// + /// The device for which to fetch the frame buffer. + /// A which manages the connection with adb. + /// The to create . + public Framebuffer(DeviceData device, IAdbClient client, Func adbSocketFactory) : this(device, client?.EndPoint!, adbSocketFactory) { } - // Initialize the headerData buffer -#if NETCORE - headerData = new byte[56]; -#else -#if !NETFRAMEWORK || NET451_OR_GREATER - int size = Marshal.SizeOf(); -#else - int size = Marshal.SizeOf(default(FramebufferHeader)); -#endif - headerData = new byte[size]; -#endif - } + /// + /// Initializes a new instance of the class. + /// + /// The device for which to fetch the frame buffer. + /// A which manages the connection with adb. + public Framebuffer(DeviceData device, IAdbClient client) : this(device, client?.EndPoint!, Factories.AdbSocketFactory) { } /// /// Initializes a new instance of the class. /// /// The device for which to fetch the frame buffer. - /// A which manages the connection with adb. - public Framebuffer(DeviceData device, AdbClient client) : this(device, client?.EndPoint) { } + /// The to create . + public Framebuffer(DeviceData device, Func adbSocketFactory) : this(device, AdbClient.DefaultEndPoint, adbSocketFactory) { } /// /// Initializes a new instance of the class. /// /// The device for which to fetch the frame buffer. - public Framebuffer(DeviceData device) : this(device, AdbClient.DefaultEndPoint) { } + public Framebuffer(DeviceData device) : this(device, AdbClient.DefaultEndPoint, Factories.AdbSocketFactory) { } /// /// Gets the device for which to fetch the frame buffer. /// - public DeviceData Device { get; } + public DeviceData Device { get; } = device ?? throw new ArgumentNullException(nameof(device)); /// /// Gets the at which the adb server is listening. /// - public EndPoint EndPoint { get; } + public EndPoint EndPoint { get; init; } = endPoint ?? throw new ArgumentNullException(nameof(endPoint)); /// /// Gets the framebuffer header. The header contains information such as the width and height and the color encoding. @@ -76,18 +81,19 @@ public Framebuffer(DeviceData device) : this(device, AdbClient.DefaultEndPoint) /// Gets the framebuffer data. You need to parse the to interpret this data (such as the color encoding). /// This property is set after you call . /// - public byte[] Data { get; private set; } + public byte[]? Data { get; private set; } /// /// Refreshes the framebuffer: fetches the latest framebuffer data from the device. Access the /// and properties to get the updated framebuffer data. /// /// Refreshes the header of framebuffer when . + [MemberNotNull(nameof(Data))] public virtual void Refresh(bool reset = false) { EnsureNotDisposed(); - using IAdbSocket socket = Factories.AdbSocketFactory(EndPoint); + using IAdbSocket socket = adbSocketFactory(EndPoint); // Select the target device socket.SetDevice(Device); @@ -100,7 +106,7 @@ public virtual void Refresh(bool reset = false) if (reset || !headerInitialized) { - Header = FramebufferHeader.Read(headerData); + Header = new FramebufferHeader(headerData); headerInitialized = true; } @@ -120,7 +126,11 @@ public virtual void Refresh(bool reset = false) } // followed by the actual framebuffer content +#if HAS_BUFFERS + _ = socket.Read(Data.AsSpan(0, (int)Header.Size)); +#else _ = socket.Read(Data, (int)Header.Size); +#endif } #if HAS_TASK @@ -135,20 +145,20 @@ public virtual async Task RefreshAsync(bool reset = false, CancellationToken can { EnsureNotDisposed(); - using IAdbSocket socket = Factories.AdbSocketFactory(EndPoint); + using IAdbSocket socket = adbSocketFactory(EndPoint); // Select the target device - socket.SetDevice(Device); + await socket.SetDeviceAsync(Device, cancellationToken).ConfigureAwait(false); // Send the framebuffer command - socket.SendAdbRequest("framebuffer:"); - socket.ReadAdbResponse(); + await socket.SendAdbRequestAsync("framebuffer:", cancellationToken).ConfigureAwait(false); + await socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); // The result first is a FramebufferHeader object, _ = await socket.ReadAsync(headerData, cancellationToken).ConfigureAwait(false); if (reset || !headerInitialized) { - Header = FramebufferHeader.Read(headerData); + Header = new FramebufferHeader(headerData); headerInitialized = true; } @@ -168,11 +178,15 @@ public virtual async Task RefreshAsync(bool reset = false, CancellationToken can } // followed by the actual framebuffer content +#if HAS_BUFFERS + _ = await socket.ReadAsync(Data.AsMemory(0, (int)Header.Size), cancellationToken).ConfigureAwait(false); +#else _ = await socket.ReadAsync(Data, (int)Header.Size, cancellationToken).ConfigureAwait(false); +#endif } #endif -#if HAS_DRAWING +#if HAS_IMAGING /// /// Converts the framebuffer data to a . /// @@ -180,7 +194,7 @@ public virtual async Task RefreshAsync(bool reset = false, CancellationToken can #if NET [SupportedOSPlatform("windows")] #endif - public virtual Bitmap ToImage() + public virtual Bitmap? ToImage() { EnsureNotDisposed(); return Data == null ? throw new InvalidOperationException($"Call {nameof(Refresh)} first") : Header.ToImage(Data); @@ -190,13 +204,13 @@ public virtual Bitmap ToImage() #if NET [SupportedOSPlatform("windows")] #endif - public static explicit operator Image(Framebuffer value) => value.ToImage(); + public static explicit operator Image?(Framebuffer value) => value.ToImage(); /// #if NET [SupportedOSPlatform("windows")] #endif - public static explicit operator Bitmap(Framebuffer value) => value.ToImage(); + public static explicit operator Bitmap?(Framebuffer value) => value.ToImage(); #endif #if WINDOWS_UWP @@ -205,7 +219,7 @@ public virtual Bitmap ToImage() /// /// A which can be used to cancel the asynchronous task. /// An which represents the framebuffer data. - public virtual Task ToBitmap(CancellationToken cancellationToken = default) + public virtual Task ToBitmap(CancellationToken cancellationToken = default) { EnsureNotDisposed(); return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmap(Data, cancellationToken); @@ -217,7 +231,7 @@ public virtual Task ToBitmap(CancellationToken cancellationToke /// The target to invoke the code on. /// A which can be used to cancel the asynchronous task. /// An which represents the framebuffer data. - public virtual Task ToBitmap(CoreDispatcher dispatcher, CancellationToken cancellationToken = default) + public virtual Task ToBitmap(CoreDispatcher dispatcher, CancellationToken cancellationToken = default) { EnsureNotDisposed(); return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmap(Data, dispatcher, cancellationToken); @@ -229,7 +243,7 @@ public virtual Task ToBitmap(CoreDispatcher dispatcher, Cancell /// The target to invoke the code on. /// A which can be used to cancel the asynchronous task. /// An which represents the framebuffer data. - public virtual Task ToBitmap(DispatcherQueue dispatcher, CancellationToken cancellationToken = default) + public virtual Task ToBitmap(DispatcherQueue dispatcher, CancellationToken cancellationToken = default) { EnsureNotDisposed(); return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmap(Data, dispatcher, cancellationToken); @@ -247,7 +261,7 @@ protected virtual void Dispose(bool disposing) ArrayPool.Shared.Return(Data, clearArray: false); } #endif - headerData = null; + headerData = null!; headerInitialized = false; disposed = true; } diff --git a/AdvancedSharpAdbClient/Models/FramebufferHeader.cs b/AdvancedSharpAdbClient/Models/FramebufferHeader.cs index dabb4cbc..a827df56 100644 --- a/AdvancedSharpAdbClient/Models/FramebufferHeader.cs +++ b/AdvancedSharpAdbClient/Models/FramebufferHeader.cs @@ -2,137 +2,221 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; -using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; + +#if WINDOWS_UWP +using System.IO; using System.Threading; +#endif -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// /// Whenever the framebuffer: service is invoked, the adb server responds with the contents /// of the framebuffer, prefixed with a object that contains more /// information about the framebuffer. /// - public struct FramebufferHeader +#if HAS_BUFFERS + [CollectionBuilder(typeof(EnumerableBuilder), nameof(EnumerableBuilder.FramebufferHeaderCreate))] +#endif + public readonly struct FramebufferHeader : IReadOnlyList { + /// + /// The length of the head when is . + /// + public const int MaxLength = 56; + + /// + /// The length of the head when is . + /// + public const int MiniLength = 52; + + /// + /// Initializes a new instance of the struct based on a byte array which contains the data. + /// + /// The data that feeds the struct. + /// As defined in +#if HAS_BUFFERS + public FramebufferHeader(ReadOnlySpan data) +#else + public FramebufferHeader(byte[] data) +#endif + { + if (data.Length is < MiniLength or > MaxLength) + { + throw new ArgumentOutOfRangeException(nameof(data), $"The length of {nameof(data)} must between {MiniLength} and {MaxLength}."); + } + + int index = 0; + + Version = ReadUInt32(in data); + + if (Version > 2) + { + // Technically, 0 is not a supported version either; we assume version 0 indicates + // an empty framebuffer. + throw new InvalidOperationException($"Framebuffer version {Version} is not supported"); + } + + Bpp = ReadUInt32(in data); + + if (Version >= 2) + { + ColorSpace = ReadUInt32(in data); + } + + Size = ReadUInt32(in data); + Width = ReadUInt32(in data); + Height = ReadUInt32(in data); + + Red = new ColorData + { + Offset = ReadUInt32(in data), + Length = ReadUInt32(in data) + }; + + Blue = new ColorData + { + Offset = ReadUInt32(in data), + Length = ReadUInt32(in data) + }; + + Green = new ColorData + { + Offset = ReadUInt32(in data), + Length = ReadUInt32(in data) + }; + + Alpha = new ColorData + { + Offset = ReadUInt32(in data), + Length = ReadUInt32(in data) + }; + +#if HAS_BUFFERS + uint ReadUInt32(in ReadOnlySpan data) +#else + uint ReadUInt32(in byte[] data) +#endif + => (uint)(data[index++] | (data[index++] << 8) | (data[index++] << 16) | (data[index++] << 24)); + } + /// /// Gets or sets the version of the framebuffer struct. /// - public uint Version { get; set; } + public uint Version { get; init; } /// /// Gets or sets the number of bytes per pixel. Usual values include 32 or 24. /// - public uint Bpp { get; set; } + public uint Bpp { get; init; } /// /// Gets or sets the color space. Only available starting with 2. /// - public uint ColorSpace { get; set; } + public uint ColorSpace { get; init; } /// /// Gets or sets the total size, in bits, of the framebuffer. /// - public uint Size { get; set; } + public uint Size { get; init; } /// /// Gets or sets the width, in pixels, of the framebuffer. /// - public uint Width { get; set; } + public uint Width { get; init; } /// /// Gets or sets the height, in pixels, of the framebuffer. /// - public uint Height { get; set; } + public uint Height { get; init; } /// /// Gets or sets information about the red color channel. /// - public ColorData Red { get; set; } + public ColorData Red { get; init; } /// /// Gets or sets information about the blue color channel. /// - public ColorData Blue { get; set; } + public ColorData Blue { get; init; } /// /// Gets or sets information about the green color channel. /// - public ColorData Green { get; set; } + public ColorData Green { get; init; } /// /// Gets or sets information about the alpha channel. /// - public ColorData Alpha { get; set; } + public ColorData Alpha { get; init; } /// - /// Creates a new object based on a byte array which contains the data. + /// The length of the head in bytes. /// - /// The data that feeds the struct. - /// A new object. - public static FramebufferHeader Read(byte[] data) - { - // as defined in https://android.googlesource.com/platform/system/core/+/master/adb/framebuffer_service.cpp - FramebufferHeader header = default; - - // Read the data from a MemoryStream so we can use the BinaryReader to process the data. - using MemoryStream stream = new(data); - using BinaryReader reader = new(stream, Encoding.ASCII -#if !NETFRAMEWORK || NET45_OR_GREATER - , leaveOpen: true -#endif - ); - header.Version = reader.ReadUInt32(); + public int Count => Version < 2 ? MiniLength : MaxLength; - if (header.Version > 2) + /// + public byte this[int index] + { + get { - // Technically, 0 is not a supported version either; we assume version 0 indicates - // an empty framebuffer. - throw new InvalidOperationException($"Framebuffer version {header.Version} is not supported"); - } + if (index < 0 || index >= Count) + { + throw new IndexOutOfRangeException("Index was out of range. Must be non-negative and less than the size of the collection."); + } - header.Bpp = reader.ReadUInt32(); + if (index > 7 && Version < 2) + { + index += 4; + } - if (header.Version >= 2) - { - header.ColorSpace = reader.ReadUInt32(); + return index switch + { + < 4 => GetByte(Version), + < 8 => GetByte(Bpp), + < 12 => GetByte(ColorSpace), + < 16 => GetByte(Size), + < 20 => GetByte(Width), + < 24 => GetByte(Height), + < 28 => GetByte(Red.Offset), + < 32 => GetByte(Red.Length), + < 36 => GetByte(Blue.Offset), + < 40 => GetByte(Blue.Length), + < 44 => GetByte(Green.Offset), + < 48 => GetByte(Green.Length), + < 52 => GetByte(Alpha.Offset), + < 56 => GetByte(Alpha.Length), + _ => throw new IndexOutOfRangeException("Index was out of range. Must be non-negative and less than the size of the collection.") + }; + + byte GetByte(uint value) => (index % 4) switch + { + 0 => (byte)value, + 1 => (byte)(value >> 8), + 2 => (byte)(value >> 16), + 3 => (byte)(value >> 24), + _ => throw new InvalidOperationException() + }; } - - header.Size = reader.ReadUInt32(); - header.Width = reader.ReadUInt32(); - header.Height = reader.ReadUInt32(); - - header.Red = new ColorData - { - Offset = reader.ReadUInt32(), - Length = reader.ReadUInt32() - }; - - header.Blue = new ColorData - { - Offset = reader.ReadUInt32(), - Length = reader.ReadUInt32() - }; - - header.Green = new ColorData - { - Offset = reader.ReadUInt32(), - Length = reader.ReadUInt32() - }; - - header.Alpha = new ColorData - { - Offset = reader.ReadUInt32(), - Length = reader.ReadUInt32() - }; - - return header; } -#if HAS_DRAWING + /// + /// Creates a new object based on a byte array which contains the data. + /// + /// The data that feeds the struct. + /// A new object. +#if HAS_BUFFERS + public static FramebufferHeader Read(ReadOnlySpan data) => new(data); +#else + public static FramebufferHeader Read(byte[] data) => new(data); +#endif + +#if HAS_IMAGING /// /// Converts a array containing the raw frame buffer data to a . /// @@ -142,7 +226,7 @@ public static FramebufferHeader Read(byte[] data) #if NET [SupportedOSPlatform("windows")] #endif - public readonly Bitmap ToImage(byte[] buffer) + public Bitmap? ToImage(byte[] buffer) { ExceptionExtensions.ThrowIfNull(buffer); @@ -172,13 +256,19 @@ public readonly Bitmap ToImage(byte[] buffer) /// /// A byte array in which the images are stored according to this . /// A that describes how the image data is represented in this . +#if HAS_BUFFERS #if NET [SupportedOSPlatform("windows")] #endif - private readonly PixelFormat StandardizePixelFormat(byte[] buffer) + private PixelFormat StandardizePixelFormat(Span buffer) +#else + private PixelFormat StandardizePixelFormat(byte[] buffer) +#endif { // Initial parameter validation. +#if !HAS_BUFFERS ExceptionExtensions.ThrowIfNull(buffer); +#endif if (buffer.Length < Width * Height * (Bpp / 8)) { @@ -208,10 +298,10 @@ private readonly PixelFormat StandardizePixelFormat(byte[] buffer) } // Get the index at which the red, bue, green and alpha values are stored. - uint redIndex = Red.Offset / 8; - uint blueIndex = Blue.Offset / 8; - uint greenIndex = Green.Offset / 8; - uint alphaIndex = Alpha.Offset / 8; + int redIndex = (int)Red.Offset / 8; + int blueIndex = (int)Blue.Offset / 8; + int greenIndex = (int)Green.Offset / 8; + int alphaIndex = (int)Alpha.Offset / 8; // Loop over the array and re-order as required for (int i = 0; i < (int)Size; i += 4) @@ -285,23 +375,24 @@ private readonly PixelFormat StandardizePixelFormat(byte[] buffer) /// A which can be used to cancel the asynchronous task. /// A that represents the image contained in the frame buffer, or /// if the framebuffer does not contain any data. This can happen when DRM is enabled on the device. - public readonly Task ToBitmap(byte[] buffer, CoreDispatcher dispatcher, CancellationToken cancellationToken = default) + public Task ToBitmap(byte[] buffer, CoreDispatcher dispatcher, CancellationToken cancellationToken = default) { - FramebufferHeader self = this; - if (dispatcher.HasThreadAccess) { return ToBitmap(buffer, cancellationToken); } else { - TaskCompletionSource taskCompletionSource = new(); + FramebufferHeader self = this; + + TaskCompletionSource taskCompletionSource = new(); _ = dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => { try { - taskCompletionSource.SetResult(await self.ToBitmap(buffer, cancellationToken)); + WriteableBitmap? result = await self.ToBitmap(buffer, cancellationToken).ConfigureAwait(false); + taskCompletionSource.SetResult(result); } catch (Exception e) { @@ -322,23 +413,24 @@ public readonly Task ToBitmap(byte[] buffer, CoreDispatcher dis /// A that represents the image contained in the frame buffer, or /// if the framebuffer does not contain any data. This can happen when DRM is enabled on the device. [ContractVersion(typeof(UniversalApiContract), 327680u)] - public readonly Task ToBitmap(byte[] buffer, DispatcherQueue dispatcher, CancellationToken cancellationToken = default) + public Task ToBitmap(byte[] buffer, DispatcherQueue dispatcher, CancellationToken cancellationToken = default) { - FramebufferHeader self = this; - if (ApiInformation.IsMethodPresent("Windows.System.DispatcherQueue", "HasThreadAccess") && dispatcher.HasThreadAccess) { return ToBitmap(buffer, cancellationToken); } else { - TaskCompletionSource taskCompletionSource = new(); + FramebufferHeader self = this; + + TaskCompletionSource taskCompletionSource = new(); if (!dispatcher.TryEnqueue(async () => { try { - taskCompletionSource.SetResult(await self.ToBitmap(buffer, cancellationToken)); + WriteableBitmap? result = await self.ToBitmap(buffer, cancellationToken).ConfigureAwait(false); + taskCompletionSource.SetResult(result); } catch (Exception e) { @@ -360,7 +452,7 @@ public readonly Task ToBitmap(byte[] buffer, DispatcherQueue di /// A which can be used to cancel the asynchronous task. /// A that represents the image contained in the frame buffer, or /// if the framebuffer does not contain any data. This can happen when DRM is enabled on the device. - public readonly async Task ToBitmap(byte[] buffer, CancellationToken cancellationToken = default) + public async Task ToBitmap(byte[] buffer, CancellationToken cancellationToken = default) { if (buffer == null) { @@ -384,7 +476,7 @@ public readonly async Task ToBitmap(byte[] buffer, Cancellation await WriteableImage.SetSourceAsync(randomAccessStream).AsTask(cancellationToken); return WriteableImage; } - catch (Exception) + catch when (!cancellationToken.IsCancellationRequested) { using InMemoryRandomAccessStream random = new(); BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, random).AsTask(cancellationToken); @@ -396,5 +488,48 @@ public readonly async Task ToBitmap(byte[] buffer, Cancellation } } #endif + + /// + public IEnumerator GetEnumerator() + { + foreach (uint value in GetEnumerable()) + { + yield return (byte)value; + yield return (byte)(value >> 8); + yield return (byte)(value >> 16); + yield return (byte)(value >> 24); + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerable GetEnumerable() + { + yield return Version; + + yield return Bpp; + + if (Version >= 2) + { + yield return ColorSpace; + } + + yield return Size; + yield return Width; + yield return Height; + + yield return Red.Offset; + yield return Red.Length; + + yield return Blue.Offset; + yield return Blue.Length; + + yield return Green.Offset; + yield return Green.Length; + + yield return Alpha.Offset; + yield return Alpha.Length; + } } } diff --git a/AdvancedSharpAdbClient/Models/NamespaceDoc.cs b/AdvancedSharpAdbClient/Models/NamespaceDoc.cs new file mode 100644 index 00000000..a60004f7 --- /dev/null +++ b/AdvancedSharpAdbClient/Models/NamespaceDoc.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace AdvancedSharpAdbClient.Models +{ + /// + /// The classes in this namespace provide models for . + /// + /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. + [CompilerGenerated] + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : AdvancedSharpAdbClient.NamespaceDoc { } +} diff --git a/AdvancedSharpAdbClient/Logs/ShellStream.cs b/AdvancedSharpAdbClient/Models/ShellStream.cs similarity index 56% rename from AdvancedSharpAdbClient/Logs/ShellStream.cs rename to AdvancedSharpAdbClient/Models/ShellStream.cs index 740ba3f2..e3a89064 100644 --- a/AdvancedSharpAdbClient/Logs/ShellStream.cs +++ b/AdvancedSharpAdbClient/Models/ShellStream.cs @@ -2,13 +2,12 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Runtime.InteropServices; using System.Threading; -namespace AdvancedSharpAdbClient.Logs +namespace AdvancedSharpAdbClient.Models { /// Represents a that wraps around an inner that contains /// output from an Android shell command. In the shell output, the LF character is replaced by a @@ -61,6 +60,102 @@ public override long Position set => throw new NotImplementedException(); } +#if HAS_BUFFERS + /// + public override int Read(Span buffer) + { + if (buffer.Length == 0) + { + return 0; + } + + // Read the raw data from the base stream. There may be a + // 'pending byte' from a previous operation; if that's the case, + // consume it. + int read; + + if (pendingByte != null) + { + buffer[0] = pendingByte.Value; + read = Inner.Read(buffer[1..]); + read++; + pendingByte = null; + } + else + { + read = Inner.Read(buffer); + } + + // Loop over the data, and find a LF (0x0d) character. If it is + // followed by a CR (0x0a) character, remove the LF chracter and + // keep only the LF character intact. + for (int i = 0; i < read - 1; i++) + { + if (buffer[i] == 0x0d && buffer[i + 1] == 0x0a) + { + buffer[i] = 0x0a; + + for (int j = i + 1; j < read - 1; j++) + { + buffer[j] = buffer[j + 1]; + } + + // Reset unused data to \0 + buffer[read - 1] = 0; + + // We have removed one byte from the array of bytes which has + // been read; but the caller asked for a fixed number of bytes. + // So we need to get the next byte from the base stream. + // If less bytes were received than asked, we know no more data is + // available so we can skip this step + if (read < buffer.Length) + { + read--; + continue; + } + + byte[] miniBuffer = new byte[1]; + int miniRead = Inner.Read(miniBuffer.AsSpan(0, 1)); + + if (miniRead == 0) + { + // If no byte was read, no more data is (currently) available, and reduce the + // number of bytes by 1. + read--; + } + else + { + // Append the byte to the buffer. + buffer[read - 1] = miniBuffer[0]; + } + } + } + + // The last byte is a special case, to find out if the next byte is 0x0a + // we need to read one more byte from the inner stream. + if (read > 0 && buffer[read - 1] == 0x0d) + { + int nextByte = Inner.ReadByte(); + + if (nextByte == 0x0a) + { + // If the next byte is 0x0a, set the last byte to 0x0a. The underlying + // stream has already advanced because of the ReadByte call, so all is good. + buffer[read - 1] = 0x0a; + } + else + { + // If the next byte was not 0x0a, store it as the 'pending byte' -- + // the next read operation will fetch this byte. We can't do a Seek here, + // because e.g. the network stream doesn't support seeking. + pendingByte = (byte)nextByte; + } + } + + return read; + } +#endif + /// public override int Read(byte[] buffer, int offset, int count) { @@ -114,8 +209,8 @@ public override int Read(byte[] buffer, int offset, int count) continue; } - byte[] minibuffer = new byte[1]; - int miniRead = Inner.Read(minibuffer, 0, 1); + byte[] miniBuffer = new byte[1]; + int miniRead = Inner.Read(miniBuffer, 1); if (miniRead == 0) { @@ -126,7 +221,7 @@ public override int Read(byte[] buffer, int offset, int count) else { // Append the byte to the buffer. - buffer[offset + read - 1] = minibuffer[0]; + buffer[offset + read - 1] = miniBuffer[0]; } } } @@ -155,40 +250,108 @@ public override int Read(byte[] buffer, int offset, int count) return read; } -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#if HAS_BUFFERS /// - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { if (buffer.Length == 0) { - return new ValueTask(0); + return 0; } - if (MemoryMarshal.TryGetArray(buffer, out ArraySegment array)) + // Read the raw data from the base stream. There may be a + // 'pending byte' from a previous operation; if that's the case, + // consume it. + int read; + + if (pendingByte != null) + { + buffer.Span[0] = pendingByte.Value; + read = await Inner.ReadAsync(buffer[1..], cancellationToken).ConfigureAwait(false); + read++; + pendingByte = null; + } + else { - return new ValueTask(ReadAsync(array.Array!, array.Offset, array.Count, cancellationToken)); + read = await Inner.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); } - byte[] sharedBuffer = ArrayPool.Shared.Rent(buffer.Length); - return FinishReadAsync(ReadAsync(sharedBuffer, 0, buffer.Length, cancellationToken), sharedBuffer, buffer); + byte[] miniBuffer = new byte[1]; + + // Loop over the data, and find a LF (0x0d) character. If it is + // followed by a CR (0x0a) character, remove the LF character and + // keep only the LF character intact. + for (int i = 0; i < read - 1; i++) + { + if (buffer.Span[i] == 0x0d && buffer.Span[i + 1] == 0x0a) + { + buffer.Span[i] = 0x0a; + + for (int j = i + 1; j < read - 1; j++) + { + buffer.Span[j] = buffer.Span[j + 1]; + } + + // Reset unused data to \0 + buffer.Span[read - 1] = 0; + + // We have removed one byte from the array of bytes which has + // been read; but the caller asked for a fixed number of bytes. + // So we need to get the next byte from the base stream. + // If less bytes were received than asked, we know no more data is + // available so we can skip this step + if (read < buffer.Length) + { + read--; + continue; + } + + int miniRead = await Inner.ReadAsync(miniBuffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); - static async ValueTask FinishReadAsync(Task readTask, byte[] localBuffer, Memory localDestination) + if (miniRead == 0) + { + // If no byte was read, no more data is (currently) available, and reduce the + // number of bytes by 1. + read--; + } + else + { + // Append the byte to the buffer. + buffer.Span[read - 1] = miniBuffer[0]; + } + } + } + + // The last byte is a special case, to find out if the next byte is 0x0a + // we need to read one more byte from the inner stream. + if (read > 0 && buffer.Span[read - 1] == 0x0d) { - try + _ = await Inner.ReadAsync(miniBuffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); + int nextByte = miniBuffer[0]; + + if (nextByte == 0x0a) { - int result = await readTask.ConfigureAwait(false); - new ReadOnlySpan(localBuffer, 0, result).CopyTo(localDestination.Span); - return result; + // If the next byte is 0x0a, set the last byte to 0x0a. The underlying + // stream has already advanced because of the ReadByte call, so all is good. + buffer.Span[read - 1] = 0x0a; } - finally + else { - ArrayPool.Shared.Return(localBuffer); + // If the next byte was not 0x0a, store it as the 'pending byte' -- + // the next read operation will fetch this byte. We can't do a Seek here, + // because e.g. the network stream doesn't support seeking. + pendingByte = (byte)nextByte; } } + + return read; } #endif #if HAS_TASK +#if NET8_0_OR_GREATER +#pragma warning disable CA1835 +#endif /// public #if !NETFRAMEWORK || NET45_OR_GREATER @@ -204,35 +367,21 @@ async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToke // Read the raw data from the base stream. There may be a // 'pending byte' from a previous operation; if that's the case, // consume it. - int read = 0; + int read; if (pendingByte != null) { buffer[offset] = pendingByte.Value; - read = -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await Inner.ReadAsync(buffer.AsMemory(offset + 1, count - 1), cancellationToken).ConfigureAwait(false); -#elif !NET35 - await Inner.ReadAsync(buffer, offset + 1, count - 1, cancellationToken).ConfigureAwait(false); -#else - await Utilities.Run(() => Inner.Read(buffer, offset + 1, count - 1)).ConfigureAwait(false); -#endif + read = await Inner.ReadAsync(buffer, offset + 1, count - 1, cancellationToken).ConfigureAwait(false); read++; pendingByte = null; } else { - read = -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await Inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); -#elif !NET35 - await Inner.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); -#else - Inner.Read(buffer, offset, count); -#endif + read = await Inner.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); } - byte[] minibuffer = new byte[1]; + byte[] miniBuffer = new byte[1]; // Loop over the data, and find a LF (0x0d) character. If it is // followed by a CR (0x0a) character, remove the LF character and @@ -262,14 +411,7 @@ async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToke continue; } - int miniRead = -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await Inner.ReadAsync(minibuffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); -#elif !NET35 - await Inner.ReadAsync(minibuffer, 0, 1, cancellationToken).ConfigureAwait(false); -#else - Inner.Read(minibuffer, 0, 1); -#endif + int miniRead = await Inner.ReadAsync(miniBuffer, 1, cancellationToken).ConfigureAwait(false); if (miniRead == 0) { @@ -280,7 +422,7 @@ async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToke else { // Append the byte to the buffer. - buffer[offset + read - 1] = minibuffer[0]; + buffer[offset + read - 1] = miniBuffer[0]; } } } @@ -289,15 +431,8 @@ async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToke // we need to read one more byte from the inner stream. if (read > 0 && buffer[offset + read - 1] == 0x0d) { - int miniRead = -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await Inner.ReadAsync(minibuffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); -#elif !NET35 - await Inner.ReadAsync(minibuffer, 0, 1, cancellationToken).ConfigureAwait(false); -#else - Inner.Read(minibuffer, 0, 1); -#endif - int nextByte = minibuffer[0]; + _ = await Inner.ReadAsync(miniBuffer, 1, cancellationToken).ConfigureAwait(false); + int nextByte = miniBuffer[0]; if (nextByte == 0x0a) { @@ -316,18 +451,25 @@ async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToke return read; } +#if NET8_0_OR_GREATER +#pragma warning restore CA1835 +#endif #endif /// + [DoesNotReturn] public override void Flush() => throw new NotImplementedException(); /// + [DoesNotReturn] public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); /// + [DoesNotReturn] public override void SetLength(long value) => throw new NotImplementedException(); /// + [DoesNotReturn] public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); /// @@ -338,7 +480,7 @@ protected override void Dispose(bool disposing) if (closeStream && Inner != null) { Inner.Dispose(); - Inner = null; + Inner = null!; } } @@ -352,7 +494,7 @@ public override async ValueTask DisposeAsync() if (closeStream && Inner != null) { await Inner.DisposeAsync(); - Inner = null; + Inner = null!; } await base.DisposeAsync(); GC.SuppressFinalize(this); diff --git a/AdvancedSharpAdbClient/Models/SyncProgressChangedEventArgs.cs b/AdvancedSharpAdbClient/Models/SyncService.EventArgs.cs similarity index 54% rename from AdvancedSharpAdbClient/Models/SyncProgressChangedEventArgs.cs rename to AdvancedSharpAdbClient/Models/SyncService.EventArgs.cs index 38cc3e7c..9503363d 100644 --- a/AdvancedSharpAdbClient/Models/SyncProgressChangedEventArgs.cs +++ b/AdvancedSharpAdbClient/Models/SyncService.EventArgs.cs @@ -1,37 +1,31 @@ -// +// // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // using System; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Models { /// - /// Provides data for the event. + /// Provides data for the event. /// - public class SyncProgressChangedEventArgs : EventArgs + public class SyncProgressChangedEventArgs(long current, long total) : EventArgs { - /// - /// Gets the number of progress percentage for the sync operation. - /// - public double ProgressPercentage => TotalBytesToReceive != 0L ? ReceivedBytesSize * 100.0 / TotalBytesToReceive : 0.0; - /// /// Gets the number of bytes sync to the local computer. /// /// An representing the number of sync bytes. - public long ReceivedBytesSize { get; } + public long ReceivedBytesSize { get; } = current; /// /// Gets the total number of bytes for the sync operation. /// /// An representing the total size of the download, in bytes. - public long TotalBytesToReceive { get; } + public long TotalBytesToReceive { get; } = total; - internal SyncProgressChangedEventArgs(long received, long total) - { - ReceivedBytesSize = received; - TotalBytesToReceive = total; - } + /// + /// Gets the number of progress percentage (from to ) for the sync operation. + /// + public double ProgressPercentage => TotalBytesToReceive != 0L ? ReceivedBytesSize * 100.0 / TotalBytesToReceive : 0.0; } } diff --git a/AdvancedSharpAdbClient/NamespaceDoc.cs b/AdvancedSharpAdbClient/NamespaceDoc.cs index bd3aab03..3151bd5e 100644 --- a/AdvancedSharpAdbClient/NamespaceDoc.cs +++ b/AdvancedSharpAdbClient/NamespaceDoc.cs @@ -2,6 +2,7 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // +using System.ComponentModel; using System.Runtime.CompilerServices; namespace AdvancedSharpAdbClient @@ -56,5 +57,6 @@ namespace AdvancedSharpAdbClient /// /// [CompilerGenerated] + [EditorBrowsable(EditorBrowsableState.Never)] internal class NamespaceDoc { } } diff --git a/AdvancedSharpAdbClient/Extensions/Attributes/CallerArgumentExpressionAttribute.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/CallerArgumentExpressionAttribute.cs similarity index 100% rename from AdvancedSharpAdbClient/Extensions/Attributes/CallerArgumentExpressionAttribute.cs rename to AdvancedSharpAdbClient/Polyfills/Attributes/CallerArgumentExpressionAttribute.cs diff --git a/AdvancedSharpAdbClient/Polyfills/Attributes/CollectionBuilderAttribute.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/CollectionBuilderAttribute.cs new file mode 100644 index 00000000..d61470c1 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Attributes/CollectionBuilderAttribute.cs @@ -0,0 +1,38 @@ +#if HAS_BUFFERS && !NET8_0_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)] + internal sealed class CollectionBuilderAttribute : Attribute + { + /// + /// Initialize the attribute to refer to the method on the type. + /// + /// The type of the builder to use to construct the collection. + /// The name of the method on the builder to use to construct the collection. + /// + /// must refer to a static method that accepts a single parameter of + /// type and returns an instance of the collection being built containing + /// a copy of the data from that span. In future releases of .NET, additional patterns may be supported. + /// + public CollectionBuilderAttribute(Type builderType, string methodName) + { + BuilderType = builderType; + MethodName = methodName; + } + + /// + /// Gets the type of the builder to use to construct the collection. + /// + public Type BuilderType { get; } + + /// + /// Gets the name of the method on the builder to use to construct the collection. + /// + /// This should match the metadata name of the target method. For example, this might be ".ctor" if targeting the type's constructor. + public string MethodName { get; } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/Attributes/ExcludeFromCodeCoverageAttribute.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/ExcludeFromCodeCoverageAttribute.cs similarity index 85% rename from AdvancedSharpAdbClient/Extensions/Attributes/ExcludeFromCodeCoverageAttribute.cs rename to AdvancedSharpAdbClient/Polyfills/Attributes/ExcludeFromCodeCoverageAttribute.cs index 93ef6252..2ac26093 100644 --- a/AdvancedSharpAdbClient/Extensions/Attributes/ExcludeFromCodeCoverageAttribute.cs +++ b/AdvancedSharpAdbClient/Polyfills/Attributes/ExcludeFromCodeCoverageAttribute.cs @@ -16,6 +16,9 @@ namespace System.Diagnostics.CodeAnalysis )] internal sealed class ExcludeFromCodeCoverageAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// public ExcludeFromCodeCoverageAttribute() { } } } diff --git a/AdvancedSharpAdbClient/Polyfills/Attributes/IsExternalInit.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/IsExternalInit.cs new file mode 100644 index 00000000..7f988bb8 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Attributes/IsExternalInit.cs @@ -0,0 +1,18 @@ +#if !NET +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/Attributes/NullableAttributes.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/NullableAttributes.cs new file mode 100644 index 00000000..adc36a56 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Attributes/NullableAttributes.cs @@ -0,0 +1,180 @@ +#if !NET5_0_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER + /// + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute + { } + + /// + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute + { } + + /// + /// Specifies that an output may be null even if the corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute + { } + + /// + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { } + + /// + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// + /// Gets the return value condition. + /// + public bool ReturnValue { get; } + } + + /// + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// + /// Gets the return value condition. + /// + public bool ReturnValue { get; } + } + + /// + /// Specifies that the output will be non-null if the named parameter is non-null. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// + /// Gets the associated parameter name. + /// + public string ParameterName { get; } + } + + /// + /// Applied to a method that will never return under any circumstance. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute + { } + + /// + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// + /// Gets the condition parameter value. + /// + public bool ParameterValue { get; } + } +#endif + + /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + public MemberNotNullAttribute(string member) => Members = [member]; + + /// + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// + /// Gets field or property member names. + /// + public string[] Members { get; } + } + + /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// The field or property member that is promised to be not-null. + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// The list of field and property members that are promised to be not-null. + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// + /// Gets the return value condition. + /// + public bool ReturnValue { get; } + + /// + /// Gets field or property member names. + /// + public string[] Members { get; } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/Attributes/SerializableAttribute.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/SerializableAttribute.cs new file mode 100644 index 00000000..38818fdb --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Attributes/SerializableAttribute.cs @@ -0,0 +1,22 @@ +#if (NETSTANDARD && !NETSTANDARD2_0_OR_GREATER) || (NETCORE && !UAP10_0_15138_0) +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System +{ + /// + /// Indicates that a class can be serialized using binary or XML serialization. This class cannot be inherited. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Delegate, Inherited = false)] + [EditorBrowsable(EditorBrowsableState.Never)] + internal sealed class SerializableAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public SerializableAttribute() { } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Extensions/Attributes/StackTraceHiddenAttribute.cs b/AdvancedSharpAdbClient/Polyfills/Attributes/StackTraceHiddenAttribute.cs similarity index 95% rename from AdvancedSharpAdbClient/Extensions/Attributes/StackTraceHiddenAttribute.cs rename to AdvancedSharpAdbClient/Polyfills/Attributes/StackTraceHiddenAttribute.cs index 4ebf69ce..232d640e 100644 --- a/AdvancedSharpAdbClient/Extensions/Attributes/StackTraceHiddenAttribute.cs +++ b/AdvancedSharpAdbClient/Polyfills/Attributes/StackTraceHiddenAttribute.cs @@ -6,7 +6,7 @@ namespace System.Diagnostics { /// /// Types and Methods attributed with StackTraceHidden will be omitted from the stack trace text shown in StackTrace.ToString() - /// and Exception.StackTrace + /// and Exception.StackTrace. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Struct, Inherited = false)] internal sealed class StackTraceHiddenAttribute : Attribute diff --git a/AdvancedSharpAdbClient/Models/DnsEndPoint.cs b/AdvancedSharpAdbClient/Polyfills/DnsEndPoint.cs similarity index 96% rename from AdvancedSharpAdbClient/Models/DnsEndPoint.cs rename to AdvancedSharpAdbClient/Polyfills/DnsEndPoint.cs index 9bd51f32..295846e9 100644 --- a/AdvancedSharpAdbClient/Models/DnsEndPoint.cs +++ b/AdvancedSharpAdbClient/Polyfills/DnsEndPoint.cs @@ -1,10 +1,10 @@ #if NETFRAMEWORK && !NET40_OR_GREATER -using AdvancedSharpAdbClient.Exceptions; using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Polyfills { /// /// Represents a network endpoint as a host name or a string representation of an IP address and a port number. @@ -54,7 +54,7 @@ not AddressFamily.InterNetworkV6 and /// /// A instance to compare to the current instance. /// if the two instances are equal; otherwise, . - public override bool Equals(object comparand) + public override bool Equals([NotNullWhen(true)] object? comparand) { return comparand is DnsEndPoint dnsComparand && _family == dnsComparand._family && diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/DateTimeExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/DateTimeExtensions.cs new file mode 100644 index 00000000..33e0d8a5 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/DateTimeExtensions.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides helper methods for working with Unix-based date formats. + /// + public static class DateTimeExtensions + { +#if NETFRAMEWORK && !NET46_OR_GREATER + // Number of 100ns ticks per time unit + internal const int MicrosecondsPerMillisecond = 1000; + private const long TicksPerMicrosecond = 10; + private const long TicksPerMillisecond = TicksPerMicrosecond * MicrosecondsPerMillisecond; + + private const int HoursPerDay = 24; + private const long TicksPerSecond = TicksPerMillisecond * 1000; + private const long TicksPerMinute = TicksPerSecond * 60; + private const long TicksPerHour = TicksPerMinute * 60; + private const long TicksPerDay = TicksPerHour * HoursPerDay; + + // Number of days in a non-leap year + private const int DaysPerYear = 365; + // Number of days in 4 years + private const int DaysPer4Years = (DaysPerYear * 4) + 1; // 1461 + // Number of days in 100 years + private const int DaysPer100Years = (DaysPer4Years * 25) - 1; // 36524 + // Number of days in 400 years + private const int DaysPer400Years = (DaysPer100Years * 4) + 1; // 146097 + + // Number of days from 1/1/0001 to 12/31/1969 + internal const int DaysTo1970 = (DaysPer400Years * 4) + (DaysPer100Years * 3) + (DaysPer4Years * 17) + DaysPerYear; // 719,162 + // Number of days from 1/1/0001 to 12/31/9999 + private const int DaysTo10000 = (DaysPer400Years * 25) - 366; // 3652059 + + internal const long MinTicks = 0; + internal const long MaxTicks = (DaysTo10000 * TicksPerDay) - 1; + + internal const long UnixEpochTicks = DaysTo1970 * TicksPerDay; + + private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 + + internal const long UnixMinSeconds = (MinTicks / TimeSpan.TicksPerSecond) - UnixEpochSeconds; + internal const long UnixMaxSeconds = (MaxTicks / TimeSpan.TicksPerSecond) - UnixEpochSeconds; +#endif + + /// + /// Gets EPOCH time. + /// + public static DateTime Epoch { get; } = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + +#if NET20 +#pragma warning disable CS1574 +#endif + /// + /// Converts a Unix time expressed as the number of seconds that have elapsed + /// since 1970-01-01T00:00:00Z to a value. + /// + /// A Unix time, expressed as the number of seconds that have elapsed + /// since 1970-01-01T00:00:00Z (January 1, 1970, at 12:00 AM UTC). For Unix times before this date, + /// its value is negative. + /// A date and time value that represents the same moment in time as the Unix time. + /// is less than -62,135,596,800. + /// -or- is greater than 253,402,300,799. + /// The Offset property value of the returned instance is + /// , which represents Coordinated Universal Time. You can convert it to the time in + /// a specific time zone by calling the method. + public static DateTimeOffset FromUnixTimeSeconds(long seconds) + { +#if NETFRAMEWORK && !NET46_OR_GREATER + if (seconds is < UnixMinSeconds or > UnixMaxSeconds) + { + throw new ArgumentOutOfRangeException(nameof(seconds), + string.Format("Valid values are between {0} and {1}, inclusive.", UnixMinSeconds, UnixMaxSeconds)); + } + + long ticks = (seconds * TimeSpan.TicksPerSecond) + UnixEpochTicks; + return new DateTimeOffset(ticks, TimeSpan.Zero); +#else + return DateTimeOffset.FromUnixTimeSeconds(seconds); +#endif + } +#if NET20 +#pragma warning restore CS1574 +#endif + +#if NETFRAMEWORK && !NET46_OR_GREATER + /// + /// Returns the number of seconds that have elapsed since 1970-01-01T00:00:00Z. + /// + /// The DateTimeOffset + /// The number of seconds that have elapsed since 1970-01-01T00:00:00Z. + public static long ToUnixTimeSeconds(this DateTimeOffset dateTimeOffset) + { + long seconds = dateTimeOffset.UtcDateTime.Ticks / TimeSpan.TicksPerSecond; + return seconds - UnixEpochSeconds; + } +#endif + + /// + /// Converts a Unix equivalent to the . + /// + /// The Unix equivalent to convert to the date. + /// A that represents the date. + public static DateTime FromUnixEpoch(long time) => Epoch.Add(new TimeSpan(time * 1000_0000)); + + /// + /// Converts a to the Unix equivalent. + /// + /// The date to convert to the Unix format. + /// A that represents the date, in Unix format. + public static long ToUnixEpoch(this DateTime date) => (long)Math.Round(date.ToUniversalTime().Subtract(Epoch).TotalSeconds); + } +} diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/EnumExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/EnumExtensions.cs new file mode 100644 index 00000000..e9718122 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/EnumExtensions.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides extension methods for the class. + /// + public static class EnumExtensions + { + /// + /// Converts the string representation of the name or numeric value of one or more + /// enumerated constants to an equivalent enumerated object. A parameter specifies + /// whether the operation is case-sensitive. The return value indicates whether the + /// conversion succeeded. + /// + /// The enumeration type to which to convert . + /// The string representation of the enumeration name or underlying value to convert. + /// to ignore case; to consider case. + /// When this method returns, contains an object of type whose + /// value is represented by if the parse operation succeeds. If the parse operation fails, + /// contains the default value of the underlying type of . This parameter is passed uninitialized. + /// if the value parameter was converted successfully; otherwise, . + /// is not an enumeration type. + public static bool TryParse(string value, bool ignoreCase, out TEnum result) where TEnum : struct + { +#if NETFRAMEWORK && !NET40_OR_GREATER + string strTypeFixed = value.Replace(' ', '_'); + if (Enum.IsDefined(typeof(TEnum), strTypeFixed)) + { + result = (TEnum)Enum.Parse(typeof(TEnum), strTypeFixed, ignoreCase); + return true; + } + else + { + foreach (string str in Enum.GetNames(typeof(TEnum))) + { + if (str.Equals(strTypeFixed, StringComparison.OrdinalIgnoreCase)) + { + result = (TEnum)Enum.Parse(typeof(TEnum), str, ignoreCase); + return true; + } + } + result = default; + return false; + } +#else + return Enum.TryParse(value, ignoreCase, out result); +#endif + } + } +} diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/EnumerableExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000..4ee7fabf --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/EnumerableExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides extension methods for the class. + /// + public static class EnumerableExtensions + { + /// + /// Adds the elements of the specified collection to the end of the . + /// + /// The type of the elements of . + /// The to be added. + /// The collection whose elements should be added to the end of the . + /// The collection itself cannot be , but it can contain elements that are + /// , if type is a reference type. + /// or is null. + public static void AddRange(this ICollection source, IEnumerable collection) + { + ExceptionExtensions.ThrowIfNull(source); + ExceptionExtensions.ThrowIfNull(collection); + + if (source is List list) + { + list.AddRange(collection); + } +#if !NETFRAMEWORK || NET40_OR_GREATER + else if (source is ISet set) + { + set.UnionWith(collection); + } +#endif + else + { + foreach (TSource item in collection) + { + source.Add(item); + } + } + } + +#if HAS_TASK + /// + /// Creates an array from a . + /// + /// The type of the elements of . + /// An to create an array from. + /// An array that contains the elements from the input sequence. + public static Task ToArrayAsync(this Task> source) => + source.ContinueWith(x => x.Result.ToArray()); + + /// + /// Creates an array from a . + /// + /// The type of the elements of . + /// An to create an array from. + /// An array that contains the elements from the input sequence. + public static Task ToArrayAsync(this IEnumerable> source) => + Extensions.WhenAll(source); +#endif + } +} diff --git a/AdvancedSharpAdbClient/Exceptions/ExceptionExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/ExceptionExtensions.cs similarity index 84% rename from AdvancedSharpAdbClient/Exceptions/ExceptionExtensions.cs rename to AdvancedSharpAdbClient/Polyfills/Extensions/ExceptionExtensions.cs index 4722c3b6..41a2f0fa 100644 --- a/AdvancedSharpAdbClient/Exceptions/ExceptionExtensions.cs +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/ExceptionExtensions.cs @@ -3,16 +3,19 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace AdvancedSharpAdbClient.Exceptions +namespace AdvancedSharpAdbClient.Polyfills { - internal static class ExceptionExtensions + /// + /// Provides extension methods for the class. + /// + public static class ExceptionExtensions { /// /// Throws an if is null. /// /// The reference type argument to validate as non-null. /// The name of the parameter with which corresponds. - public static void ThrowIfNull(object argument, [CallerArgumentExpression(nameof(argument))] string paramName = null) + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(argument, paramName); @@ -30,7 +33,7 @@ public static void ThrowIfNull(object argument, [CallerArgumentExpression(nameof /// The argument to validate as less or equal than . /// The value to compare with . /// The name of the parameter with which corresponds. - public static void ThrowIfGreaterThan(T value, T other, [CallerArgumentExpression(nameof(value))] string paramName = null) + public static void ThrowIfGreaterThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable { #if NET8_0_OR_GREATER @@ -49,7 +52,7 @@ public static void ThrowIfGreaterThan(T value, T other, [CallerArgumentExpres /// The argument to validate as greater than or equal than . /// The value to compare with . /// The name of the parameter with which corresponds. - public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string paramName = null) + public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable { #if NET8_0_OR_GREATER @@ -68,12 +71,12 @@ public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpressio /// The argument to validate as non-negative. /// The name of the parameter with which corresponds. #if NET8_0_OR_GREATER - public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string paramName = null) + public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase { ArgumentOutOfRangeException.ThrowIfNegative(value, paramName); #else - public static void ThrowIfNegative(int value, [CallerArgumentExpression(nameof(value))] string paramName = null) + public static void ThrowIfNegative(double value, [CallerArgumentExpression(nameof(value))] string? paramName = null) { if (value < 0) { @@ -89,11 +92,7 @@ public static void ThrowIfNegative(int value, [CallerArgumentExpression(nameof(v /// The object whose type's full name should be included in any resulting . /// The is . [StackTraceHidden] - public static void ThrowIf( -#if HAS_INDEXRANGE - [DoesNotReturnIf(true)] -#endif - bool condition, object instance) + public static void ThrowIf([DoesNotReturnIf(true)] bool condition, object instance) { #if NET7_0_OR_GREATER ObjectDisposedException.ThrowIf(condition, instance); diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/SocketExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/SocketExtensions.cs new file mode 100644 index 00000000..1dbf03b0 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/SocketExtensions.cs @@ -0,0 +1,189 @@ +#if HAS_TASK +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; +using System.Net.Sockets; +using System.Threading; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides extension methods for the class. + /// + public static class SocketExtensions + { +#if !HAS_BUFFERS + /// + /// Asynchronously receives data from a connected socket. + /// + /// The socket from which to read data. + /// An array of type that is the storage location for the received data. + /// A bitwise combination of the values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public static Task ReceiveAsync(this Socket socket, byte[] buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) + => socket.ReceiveAsync(buffer, 0, buffer.Length, socketFlags, cancellationToken); +#endif + + /// + /// Asynchronously receives data from a connected socket. + /// + /// The socket from which to read data. + /// An array of type that is the storage location for the received data. + /// The number of bytes to receive. + /// A bitwise combination of the values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public static Task ReceiveAsync(this Socket socket, byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) +#if HAS_BUFFERS + => socket.ReceiveAsync(buffer.AsMemory(0, size), socketFlags, cancellationToken).AsTask(); +#else + => socket.ReceiveAsync(buffer, 0, size, socketFlags, cancellationToken); +#endif + + /// + /// Asynchronously receives data from a connected socket. + /// + /// The socket from which to read data. + /// An array of type that is the storage location for the received data. + /// The zero-based position in the parameter at which to start storing data. + /// The number of bytes to receive. + /// A bitwise combination of the values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public static Task ReceiveAsync(this Socket socket, byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) + { +#if HAS_BUFFERS + return socket.ReceiveAsync(buffer.AsMemory(offset, size), socketFlags, cancellationToken).AsTask(); +#elif HAS_PROCESS + + // Register a callback so that when a cancellation is requested, the socket is closed. + // This will cause an ObjectDisposedException to bubble up via TrySetResult, which we can catch + // and convert to a TaskCancelledException - which is the exception we expect. + CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(socket.Close); + + TaskCompletionSource taskCompletionSource = new(socket); + + IAsyncResult asyncResult = socket.BeginReceive(buffer, offset, size, socketFlags, iar => + { + // this is the callback + + TaskCompletionSource taskCompletionSource = (TaskCompletionSource)iar.AsyncState; + Socket socket = (Socket)taskCompletionSource.Task.AsyncState; + + try + { + taskCompletionSource.TrySetResult(socket.EndReceive(iar)); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + taskCompletionSource.TrySetCanceled(); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + finally + { + cancellationTokenRegistration.Dispose(); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; +#else + return Extensions.Run(() => socket.Receive(buffer, offset, size, socketFlags), cancellationToken); +#endif + } + +#if !HAS_BUFFERS + /// + /// Asynchronously sends data to a connected socket. + /// + /// The socket from which to send data. + /// An array of type that contains the data to be sent. + /// A bitwise combination of the values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public static Task SendAsync(this Socket socket, byte[] buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) + => socket.SendAsync(buffer, 0, buffer.Length, socketFlags, cancellationToken); +#endif + + /// + /// Asynchronously sends data to a connected socket. + /// + /// The socket from which to send data. + /// An array of type that contains the data to be sent. + /// The number of bytes to send. + /// A bitwise combination of the values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public static Task SendAsync(this Socket socket, byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) +#if HAS_BUFFERS + => socket.SendAsync(buffer.AsMemory(0, size), socketFlags, cancellationToken).AsTask(); +#else + => socket.SendAsync(buffer, 0, size, socketFlags, cancellationToken); +#endif + + /// + /// Asynchronously sends data to a connected socket. + /// + /// The socket from which to send data. + /// An array of type that contains the data to be sent. + /// The position in the data buffer at which to begin sending data. + /// The number of bytes to send. + /// A bitwise combination of the values. + /// A which can be used to cancel the asynchronous task. + /// Cancelling the task will also close the socket. + /// The number of bytes received. + public static Task SendAsync(this Socket socket, byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) + { +#if HAS_BUFFERS + return socket.SendAsync(buffer.AsMemory(offset, size), socketFlags, cancellationToken).AsTask(); +#elif HAS_PROCESS + // Register a callback so that when a cancellation is requested, the socket is closed. + // This will cause an ObjectDisposedException to bubble up via TrySetResult, which we can catch + // and convert to a TaskCancelledException - which is the exception we expect. + CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(socket.Dispose); + + TaskCompletionSource taskCompletionSource = new(socket); + + _ = socket.BeginSend(buffer, offset, size, socketFlags, iar => + { + // this is the callback + + TaskCompletionSource taskCompletionSource = (TaskCompletionSource)iar.AsyncState; + Socket socket = (Socket)taskCompletionSource.Task.AsyncState; + + try + { + taskCompletionSource.TrySetResult(socket.EndSend(iar)); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + taskCompletionSource.TrySetCanceled(); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + finally + { + cancellationTokenRegistration.Dispose(); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; +#else + return Extensions.Run(() => socket.Send(buffer, offset, size, socketFlags), cancellationToken); +#endif + } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/StreamExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/StreamExtensions.cs new file mode 100644 index 00000000..6ffae039 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/StreamExtensions.cs @@ -0,0 +1,216 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; +using System.IO; +using System.Threading; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides extension methods for the class. + /// + public static class StreamExtensions + { +#if !HAS_BUFFERS + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// The stream from which to read data. + /// An array of bytes. When this method returns, the contents of this region are replaced by the bytes read from the current source. + /// The total number of bytes read into the buffer. This can be less than the size of the buffer if that many bytes are not currently available, + /// or zero (0) if the buffer's length is zero or the end of the stream has been reached. + public static int Read(this Stream stream, byte[] buffer) => + stream.Read(buffer, 0, buffer.Length); + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// The stream from which to read data. + /// An array of bytes. This method copies the contents of this region to the current stream. + public static void Write(this Stream stream, byte[] buffer) => + stream.Write(buffer, 0, buffer.Length); + +#if HAS_TASK + /// + /// Asynchronously reads a sequence of bytes from the current stream, within monitors cancellation requests. + /// + /// The stream from which to read data. + /// The buffer to write the data into. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous read operation. The value of the TResult parameter + /// contains the total number of bytes read into the buffer. The result value can be less than the number + /// of bytes requested if the number of bytes currently available is less than the requested number, + /// or it can be 0 (zero) if length of the buffer is 0 or if the end of the stream has been reached. + /// Cancelling the task will also close the stream. + public static Task ReadAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default) => + stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + + /// + /// Asynchronously writes a sequence of bytes to the current stream, within this stream by the number of bytes written. + /// + /// The stream from which to write data. + /// The buffer to write data from. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// Cancelling the task will also close the stream. + public static Task WriteAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default) => + stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); +#endif +#endif + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// The stream from which to read data. + /// An array of bytes. When this method returns, the contents of this region are replaced by the bytes read from the current source. + /// The maximum number of bytes to read. + /// The total number of bytes read into the buffer. This can be less than the size of the buffer if that many bytes are not currently available, + /// or zero (0) if the buffer's length is zero or the end of the stream has been reached. + public static int Read(this Stream stream, byte[] buffer, int count) => + stream.Read(buffer, 0, count); + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// The stream from which to read data. + /// An array of bytes. This method copies bytes from buffer to the current stream. + /// The number of bytes to be written to the current stream. + public static void Write(this Stream stream, byte[] buffer, int count) => + stream.Write(buffer, 0, count); + +#if HAS_TASK + /// + /// Asynchronously reads a sequence of bytes from the current stream, advances the position + /// within the stream by the number of bytes read, and monitors cancellation requests. + /// + /// The stream from which to read data. + /// The buffer to write the data into. + /// The maximum number of bytes to read. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous read operation. The value of the TResult parameter + /// contains the total number of bytes read into the buffer. The result value can be less than the number + /// of bytes requested if the number of bytes currently available is less than the requested number, + /// or it can be 0 (zero) if is 0 or if the end of the stream has been reached. + /// Cancelling the task will also close the stream. + public static Task ReadAsync(this Stream stream, byte[] buffer, int count, CancellationToken cancellationToken = default) => + stream.ReadAsync(buffer, 0, count, cancellationToken); + + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position + /// within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// The stream from which to write data. + /// The buffer to write data from. + /// The maximum number of bytes to write. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// Cancelling the task will also close the stream. + public static Task WriteAsync(this Stream stream, byte[] buffer, int count, CancellationToken cancellationToken = default) => + stream.WriteAsync(buffer, 0, count, cancellationToken); + +#if NET35 + /// + /// Asynchronously reads a sequence of bytes from the current stream, advances the position + /// within the stream by the number of bytes read, and monitors cancellation requests. + /// + /// The stream from which to read data. + /// The buffer to write the data into. + /// The byte offset in at which to begin writing data from the stream. + /// The maximum number of bytes to read. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous read operation. The value of the TResult parameter + /// contains the total number of bytes read into the buffer. The result value can be less than the number + /// of bytes requested if the number of bytes currently available is less than the requested number, + /// or it can be 0 (zero) if is 0 or if the end of the stream has been reached. + /// Cancelling the task will also close the stream. + public static Task ReadAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + // Register a callback so that when a cancellation is requested, the socket is closed. + // This will cause an ObjectDisposedException to bubble up via TrySetResult, which we can catch + // and convert to a TaskCancelledException - which is the exception we expect. + CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(stream.Close); + + TaskCompletionSource taskCompletionSource = new(stream); + + IAsyncResult asyncResult = stream.BeginRead(buffer, offset, count, iar => + { + // this is the callback + + TaskCompletionSource taskCompletionSource = (TaskCompletionSource)iar.AsyncState; + Stream stream = (Stream)taskCompletionSource.Task.AsyncState; + + try + { + taskCompletionSource.TrySetResult(stream.EndRead(iar)); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + taskCompletionSource.TrySetCanceled(); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + finally + { + cancellationTokenRegistration.Dispose(); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; + } + + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position + /// within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// The stream from which to write data. + /// The buffer to write data from. + /// The zero-based byte offset in from which to begin copying bytes to the stream. + /// The maximum number of bytes to write. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + /// Cancelling the task will also close the stream. + public static Task WriteAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + // Register a callback so that when a cancellation is requested, the socket is closed. + // This will cause an ObjectDisposedException to bubble up via TrySetResult, which we can catch + // and convert to a TaskCancelledException - which is the exception we expect. + CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(stream.Close); + + TaskCompletionSource taskCompletionSource = new(stream); + + IAsyncResult asyncResult = stream.BeginWrite(buffer, offset, count, iar => + { + // this is the callback + + TaskCompletionSource taskCompletionSource = (TaskCompletionSource)iar.AsyncState; + Stream stream = (Stream)taskCompletionSource.Task.AsyncState; + + try + { + stream.EndWrite(iar); + taskCompletionSource.TrySetResult(null); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + taskCompletionSource.TrySetCanceled(); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + finally + { + cancellationTokenRegistration.Dispose(); + } + }, taskCompletionSource); + + return taskCompletionSource.Task; + } +#endif +#endif + } +} diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/StringBuilderExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/StringBuilderExtensions.cs new file mode 100644 index 00000000..d3480120 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,27 @@ +#if NETFRAMEWORK && !NET40_OR_GREATER +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System.Text; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides extension methods for the class. + /// + public static class StringBuilderExtensions + { + /// + /// Removes all characters from the current instance. + /// + /// The to removes all characters. + /// An object whose is 0 (zero). + public static StringBuilder Clear(this StringBuilder builder) + { + builder.Length = 0; + return builder; + } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/Extensions/StringExtensions.cs b/AdvancedSharpAdbClient/Polyfills/Extensions/StringExtensions.cs new file mode 100644 index 00000000..1b1db8ba --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Extensions/StringExtensions.cs @@ -0,0 +1,185 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Provides extension methods for the class. + /// + public static class StringExtensions + { + /// + /// Indicates whether a specified string is , empty, or consists only of white-space characters. + /// + /// The string to test. + /// if the parameter is or + /// , or if consists exclusively of white-space characters. + public static bool IsNullOrWhiteSpace(string? value) + { +#if NETFRAMEWORK && !NET40_OR_GREATER + if (value == null) + { + return true; + } + + for (int i = 0; i < value.Length; i++) + { + if (!char.IsWhiteSpace(value[i])) + { + return false; + } + } + + return true; +#else + return string.IsNullOrWhiteSpace(value); +#endif + } + +#if !HAS_FULLSTRING + /// + /// Returns a value indicating whether a specified string occurs within this string, using the specified comparison rules. + /// + /// A sequence in which to locate a value. + /// The string to seek. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if the parameter occurs within this string, + /// or if is the empty string (""); otherwise, . + public static bool Contains(this string text, string value, StringComparison comparisonType) => + text.IndexOf(value, comparisonType) != -1; + + /// + /// Splits a string into substrings based on a specified delimiting character and, optionally, options. + /// + /// The string to split. + /// A character that delimits the substrings in this string. + /// A bitwise combination of the enumeration values that specifies whether to trim substrings and include empty substrings. + /// An array whose elements contain the substrings from this instance that are delimited by . + public static string[] Split(this string text, char separator, StringSplitOptions options = StringSplitOptions.None) => + text.Split(new[] { separator }, options); + + /// + /// Splits a string into a maximum number of substrings based on a specified delimiting + /// character and, optionally, options. Splits a string into a maximum number of + /// substrings based on the provided character separator, optionally omitting empty + /// substrings from the result. + /// + /// The string to split. + /// A character that delimits the substrings in this string. + /// The maximum number of elements expected in the array. + /// A bitwise combination of the enumeration values that specifies whether to trim substrings and include empty substrings. + /// An array that contains at most count substrings from this instance that are delimited by . + public static string[] Split(this string text, char separator, int count, StringSplitOptions options = StringSplitOptions.None) => + text.Split(new[] { separator }, count, options); + + /// + /// Determines whether this string instance starts with the specified character. + /// + /// A sequence in which to locate a value. + /// The character to compare. + /// if matches the beginning of this string; otherwise, . + public static bool StartsWith(this string text, char value) => text.StartsWith(new string([value])); + + /// + /// Determines whether the end of this string instance matches the specified character. + /// + /// A sequence in which to locate a value. + /// The character to compare to the character at the end of this instance. + /// if matches the end of this instance; otherwise, . + public static bool EndsWith(this string text, char value) => text.EndsWith(new string([value])); +#endif + + /// + /// Concatenates the elements of an object array, using the specified separator between each element. + /// + /// The string to use as a separator. is included + /// in the returned string only if has more than one element. + /// An array that contains the elements to concatenate. + /// A string that consists of the elements of delimited by the + /// string.-or- if values has zero elements. + public static string Join(string? separator, params object?[] values) + { +#if NETFRAMEWORK && !NET40_OR_GREATER + ExceptionExtensions.ThrowIfNull(values); + + if (values.Length == 0 || values[0] == null) + { + return string.Empty; + } + + separator ??= string.Empty; + + StringBuilder stringBuilder = new(); + string? text = values[0]?.ToString(); + if (text != null) + { + _ = stringBuilder.Append(text); + } + + for (int i = 1; i < values.Length; i++) + { + _ = stringBuilder.Append(separator); + if (values[i] != null) + { + text = values[i]?.ToString(); + if (text != null) + { + _ = stringBuilder.Append(text); + } + } + } + + return stringBuilder.ToString(); +#else + return string.Join(separator, values); +#endif + } + + /// + /// Concatenates the members of a constructed collection of type , + /// using the specified separator between each member. + /// + /// The string to use as a separator. is included + /// in the returned string only if has more than one element. + /// A collection that contains the strings to concatenate. + /// A string that consists of the elements of delimited by the + /// string.-or- if values has zero elements. + public static string Join(string? separator, IEnumerable values) + { +#if NETFRAMEWORK && !NET40_OR_GREATER + ExceptionExtensions.ThrowIfNull(values); + + separator ??= string.Empty; + + using IEnumerator enumerator = values.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return string.Empty; + } + + StringBuilder result = new(); + if (enumerator.Current != null) + { + _ = result.Append(enumerator.Current); + } + + while (enumerator.MoveNext()) + { + _ = result.Append(separator); + if (enumerator.Current != null) + { + _ = result.Append(enumerator.Current); + } + } + return result.ToString(); +#else + return string.Join(separator, values); +#endif + } + } +} diff --git a/AdvancedSharpAdbClient/Polyfills/HashCode.cs b/AdvancedSharpAdbClient/Polyfills/HashCode.cs new file mode 100644 index 00000000..a88fa196 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/HashCode.cs @@ -0,0 +1,574 @@ +#if !NET461_OR_GREATER && !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_0_OR_GREATER && !UAP10_0_15138_0 +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +/* + +The xxHash32 implementation is based on the code published by Yann Collet: +https://raw.githubusercontent.com/Cyan4973/xxHash/5c174cfa4e45a42f94082dc0d4539b39696afea1/xxhash.c + + xxHash - Fast Hash algorithm + Copyright (C) 2012-2016, Yann Collet + + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + You can contact the author at : + - xxHash homepage: http://www.xxhash.com + - xxHash source repository : https://github.com/Cyan4973/xxHash + +*/ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace AdvancedSharpAdbClient.Polyfills +{ + // xxHash32 is used for the hash code. + // https://github.com/Cyan4973/xxHash + + /// + /// Combines the hash code for multiple values into a single hash code. + /// + public struct HashCode + { + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private uint _v1, _v2, _v3, _v4; + private uint _queue1, _queue2, _queue3; + private uint _length; + + private static uint GenerateGlobalSeed() => (uint)new Random().Next(int.MinValue, int.MaxValue); + + /// + /// Diffuses the hash code returned by the specified value. + /// + /// The type of the value to add the hash code. + /// The value to add to the hash code. + /// The hash code that represents the single value. + public static int Combine(T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 4; + + hash = QueueRound(hash, hc1); + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines two values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The hash code that represents the two values. + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines three values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The hash code that represents the three values. + public static int Combine(T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 12; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = QueueRound(hash, hc3); + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines four values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The hash code that represents the four values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 16; + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines five values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The hash code that represents the five values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 20; + + hash = QueueRound(hash, hc5); + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines six values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The hash code that represents the six values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 24; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines seven values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The type of the seventh value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The seventh value to combine into the hash code. + /// The hash code that represents the seven values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 28; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = QueueRound(hash, hc7); + + hash = MixFinal(hash); + return (int)hash; + } + + /// + /// Combines eight values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The type of the seventh value to combine into the hash code. + /// The type of the eighth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The seventh value to combine into the hash code. + /// The eighth value to combine into the hash code. + /// The hash code that represents the eight values. + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + uint hc8 = (uint)(value8?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + v1 = Round(v1, hc5); + v2 = Round(v2, hc6); + v3 = Round(v3, hc7); + v4 = Round(v4, hc8); + + uint hash = MixState(v1, v2, v3, v4); + hash += 32; + + hash = MixFinal(hash); + return (int)hash; + } + + [MethodImpl((MethodImplOptions)256)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = s_seed + Prime1 + Prime2; + v2 = s_seed + Prime2; + v3 = s_seed; + v4 = s_seed - Prime1; + } + + [MethodImpl((MethodImplOptions)256)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + (input * Prime2), 13) * Prime1; + } + + [MethodImpl((MethodImplOptions)256)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; + } + + [MethodImpl((MethodImplOptions)256)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl((MethodImplOptions)256)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl((MethodImplOptions)256)] + private static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); + + /// + /// Adds a single value to the hash code. + /// + /// The type of the value to add to the hash code. + /// The value to add to the hash code. + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + /// + /// Adds a single value to the hash code, specifying the type that provides the hash code function. + /// + /// The type of the value to add to the hash code. + /// The value to add to the hash code. + /// The to use to calculate the hash code. This value can be a null reference (Nothing in Visual Basic), which will use the default equality comparer for . + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + private void Add(int value) + { + // The original xxHash works as follows: + // 0. Initialize immediately. We can't do this in a struct (no + // default ctor). + // 1. Accumulate blocks of length 16 (4 uints) into 4 accumulators. + // 2. Accumulate remaining blocks of length 4 (1 uint) into the + // hash. + // 3. Accumulate remaining blocks of length 1 into the hash. + + // There is no need for #3 as this type only accepts ints. _queue1, + // _queue2 and _queue3 are basically a buffer so that when + // ToHashCode is called we can execute #2 correctly. + + // We need to initialize the xxHash32 state (_v1 to _v4) lazily (see + // #0) nd the last place that can be done if you look at the + // original code is just before the first block of 16 bytes is mixed + // in. The xxHash32 state is never used for streams containing fewer + // than 16 bytes. + + // To see what's really going on here, have a look at the Combine + // methods. + + uint val = (uint)value; + + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint previousLength = _length++; + uint position = previousLength % 4; + + // Switch can't be inlined. + + if (position == 0) + { + _queue1 = val; + } + else if (position == 1) + { + _queue2 = val; + } + else if (position == 2) + { + _queue3 = val; + } + else // position == 3 + { + if (previousLength == 3) + { + Initialize(out _v1, out _v2, out _v3, out _v4); + } + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, val); + } + } + + /// + /// Calculates the final hash code after consecutive invocations. + /// + /// The calculated hash code. + public readonly int ToHashCode() + { + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint length = _length; + + // position refers to the *next* queue position in this method, so + // position == 1 means that _queue1 is populated; _queue2 would have + // been populated on the next call to Add. + uint position = length % 4; + + // If the length is less than 4, _v1 to _v4 don't contain anything + // yet. xxHash32 treats this differently. + + uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + + // _length is incremented once per Add(Int32) and is therefore 4 + // times too small (xxHash length is in bytes, not ints). + + hash += length * 4; + + // Mix what remains in the queue + + // Switch can't be inlined right now, so use as few branches as + // possible by manually excluding impossible scenarios (position > 1 + // is always false if position is not > 0). + if (position > 0) + { + hash = QueueRound(hash, _queue1); + if (position > 1) + { + hash = QueueRound(hash, _queue2); + if (position > 2) + { + hash = QueueRound(hash, _queue3); + } + } + } + + hash = MixFinal(hash); + return (int)hash; + } + +#pragma warning disable CS0809 + // Obsolete member 'memberA' overrides non-obsolete member 'memberB'. + // Disallowing GetHashCode and Equals is by design + + // * We decided to not override GetHashCode() to produce the hash code + // as this would be weird, both naming-wise as well as from a + // behavioral standpoint (GetHashCode() should return the object's + // hash code, not the one being computed). + + // * Even though ToHashCode() can be called safely multiple times on + // this implementation, it is not part of the contract. If the + // implementation has to change in the future we don't want to worry + // about people who might have incorrectly used this type. + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + [DoesNotReturn] + public override readonly int GetHashCode() => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code."); + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + [DoesNotReturn] + public override readonly bool Equals(object? obj) => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes."); + + /// + public static bool operator ==(HashCode left, HashCode right) => left.Equals(right); + + /// + public static bool operator !=(HashCode left, HashCode right) => !(left == right); +#pragma warning restore CS0809 + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Interfaces/IProgress.cs b/AdvancedSharpAdbClient/Polyfills/Interfaces/IProgress.cs similarity index 92% rename from AdvancedSharpAdbClient/Interfaces/IProgress.cs rename to AdvancedSharpAdbClient/Polyfills/Interfaces/IProgress.cs index 015e9510..5c88a752 100644 --- a/AdvancedSharpAdbClient/Interfaces/IProgress.cs +++ b/AdvancedSharpAdbClient/Polyfills/Interfaces/IProgress.cs @@ -2,7 +2,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Polyfills { /// Defines a provider for progress updates. /// The type of progress update value. diff --git a/AdvancedSharpAdbClient/Polyfills/Interfaces/IReadOnlyCollection.cs b/AdvancedSharpAdbClient/Polyfills/Interfaces/IReadOnlyCollection.cs new file mode 100644 index 00000000..cb3ca93c --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Interfaces/IReadOnlyCollection.cs @@ -0,0 +1,22 @@ +#if NETFRAMEWORK && !NET45_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Represents a strongly-typed, read-only collection of elements. + /// + /// The type of the elements. + public interface IReadOnlyCollection : IEnumerable + { + /// + /// Gets the number of elements in the collection. + /// + /// The number of elements in the collection. + int Count { get; } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/Interfaces/IReadOnlyList.cs b/AdvancedSharpAdbClient/Polyfills/Interfaces/IReadOnlyList.cs new file mode 100644 index 00000000..07822794 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Interfaces/IReadOnlyList.cs @@ -0,0 +1,21 @@ +#if NETFRAMEWORK && !NET45_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Represents a read-only collection of elements that can be accessed by index. + /// + /// The type of elements in the read-only list. + public interface IReadOnlyList : IReadOnlyCollection + { + /// + /// Gets the element at the specified index in the read-only list. + /// + /// The zero-based index of the element to get. + /// The element at the specified index in the read-only list. + T this[int index] { get; } + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/NamespaceDoc.cs b/AdvancedSharpAdbClient/Polyfills/NamespaceDoc.cs new file mode 100644 index 00000000..d992eddf --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/NamespaceDoc.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// The classes in this namespace provide polyfills. + /// + /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. + [CompilerGenerated] + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : AdvancedSharpAdbClient.NamespaceDoc { } +} diff --git a/AdvancedSharpAdbClient/Polyfills/Point.cs b/AdvancedSharpAdbClient/Polyfills/Point.cs new file mode 100644 index 00000000..9f6ac9e0 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Point.cs @@ -0,0 +1,152 @@ +#if !HAS_DRAWING +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Represents an ordered pair of x and y coordinates that define a point in a two-dimensional plane. + /// + public struct Point : IEquatable + { + /// + /// Creates a new instance of the class with member data left uninitialized. + /// + public static readonly Point Empty; + + private int x; // Do not rename (binary serialization) + private int y; // Do not rename (binary serialization) + + /// + /// Initializes a new instance of the class with the specified coordinates. + /// + /// The horizontal "X" coordinate. + /// The vertical "Y" coordinate. + public Point(int x, int y) + { + this.x = x; + this.y = y; + } + + /// + /// Initializes a new instance of the Point class using coordinates specified by an integer value. + /// + public Point(int dw) + { + x = LowInt16(dw); + y = HighInt16(dw); + } + + /// + /// Gets a value indicating whether this is empty. + /// + public readonly bool IsEmpty => x == 0 && y == 0; + + /// + /// Gets the x-coordinate of this . + /// + public int X + { + readonly get => x; + set => x = value; + } + + /// + /// Gets the y-coordinate of this . + /// + public int Y + { + readonly get => y; + set => y = value; + } + + /// + /// Compares two objects. The result specifies whether the values of the + /// and properties of the two + /// objects are equal. + /// + /// A to compare. + /// A to compare. + /// if the and values + /// of and are equal; otherwise, . + public static bool operator ==(Point left, Point right) => left.X == right.X && left.Y == right.Y; + + /// + /// Compares two objects. The result specifies whether the values of the + /// or properties of the two + /// objects are unequal. + /// + /// A to compare. + /// A to compare. + /// if the values of either the or values + /// of and differ; otherwise, . + public static bool operator !=(Point left, Point right) => !(left == right); + + /// + /// Specifies whether this contains the same coordinates as the specified + /// . + /// + /// The to test for equality. + /// if is a and has the same coordinates as this point instance. + public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is Point point && Equals(point); + + /// + /// Specifies whether this contains the same coordinates as the specified + /// . + /// + /// The point to test for equality. + /// if has the same coordinates as this point instance. + public readonly bool Equals(Point other) => this == other; + + /// + /// Returns a hash code. + /// + /// An integer value that specifies a hash value for this . + public override readonly int GetHashCode() => HashCode.Combine(X, Y); + + /// + /// Translates this by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + public void Offset(int dx, int dy) + { + unchecked + { + X += dx; + Y += dy; + } + } + + /// + /// Translates this by the specified amount. + /// + /// The used offset this . + public void Offset(Point p) => Offset(p.X, p.Y); + + /// + /// Deconstruct the class. + /// + /// The horizontal "X" coordinate. + /// The vertical "Y" coordinate. + public readonly void Deconstruct(out int cx, out int cy) + { + cx = X; + cy = Y; + } + + /// + /// Converts this to a human readable string. + /// + /// A string that represents this . + public override readonly string ToString() => $"{{X={X},Y={Y}}}"; + + private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff)); + + private static short LowInt16(int n) => unchecked((short)(n & 0xffff)); + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Polyfills/Rectangle.cs b/AdvancedSharpAdbClient/Polyfills/Rectangle.cs new file mode 100644 index 00000000..520e97a4 --- /dev/null +++ b/AdvancedSharpAdbClient/Polyfills/Rectangle.cs @@ -0,0 +1,283 @@ +#if !HAS_DRAWING +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AdvancedSharpAdbClient.Polyfills +{ + /// + /// Stores the location and size of a rectangular region. + /// + /// The x-coordinate of the upper-left corner of the rectangle. + /// The y-coordinate of the upper-left corner of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public struct Rectangle(int x, int y, int width, int height) : IEquatable + { + /// + /// Represents a structure with its properties left uninitialized. + /// + public static readonly Rectangle Empty; + + /// + /// Creates a new with the specified location and size. + /// + /// The x-coordinate of the upper-left corner of this structure. + /// The y-coordinate of the upper-left corner of this structure. + /// The x-coordinate of the lower-right corner of this structure. + /// The y-coordinate of the lower-right corner of this structure. + /// The new that this method creates. + public static Rectangle FromLTRB(int left, int top, int right, int bottom) => + new(left, top, unchecked(right - left), unchecked(bottom - top)); + + /// + /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this + /// . + /// + public Point Location + { + readonly get => new(X, Y); + set + { + X = value.X; + Y = value.Y; + } + } + + /// + /// Gets or sets the x-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public int X { readonly get; set; } = x; + + /// + /// Gets or sets the y-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public int Y { readonly get; set; } = y; + + /// + /// Gets or sets the width of the rectangular region defined by this . + /// + public int Width { readonly get; set; } = width; + + /// + /// Gets or sets the width of the rectangular region defined by this . + /// + public int Height { readonly get; set; } = height; + + /// + /// Gets the x-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public readonly int Left => X; + + /// + /// Gets the y-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public readonly int Top => Y; + + /// + /// Gets the x-coordinate of the lower-right corner of the rectangular region defined by this + /// . + /// + public readonly int Right => unchecked(X + Width); + + /// + /// Gets the y-coordinate of the lower-right corner of the rectangular region defined by this + /// . + /// + public readonly int Bottom => unchecked(Y + Height); + + /// + /// Tests whether this has a + /// or a of 0. + /// + public readonly bool IsEmpty => Height == 0 && Width == 0 && X == 0 && Y == 0; + + /// + /// Tests whether is a with the same location + /// and size of this Rectangle. + /// + /// The to test. + /// This method returns if is a structure + /// and its , , , and properties are equal to + /// the corresponding properties of this structure; otherwise, . + public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is Rectangle rectangle && Equals(rectangle); + + /// + public readonly bool Equals(Rectangle other) => this == other; + + /// + /// Tests whether two objects have equal location and size. + /// + /// The Rectangle structure that is to the left of the equality operator. + /// The Rectangle structure that is to the right of the equality operator. + /// This operator returns if the two structures have equal + /// , , , and properties. + public static bool operator ==(Rectangle left, Rectangle right) => + left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height; + + /// + /// Tests whether two objects differ in location or size. + /// + /// The Rectangle structure that is to the left of the inequality operator. + /// The Rectangle structure that is to the right of the inequality operator. + /// This operator returns if any of the , , + /// properties of the two structures are unequal; otherwise . + public static bool operator !=(Rectangle left, Rectangle right) => !(left == right); + + /// + /// Determines if the specified point is contained within the rectangular region defined by this + /// . + /// + /// The x-coordinate of the point to test. + /// The y-coordinate of the point to test. + /// This method returns if the point defined by and + /// is contained within this structure; otherwise . + public readonly bool Contains(int x, int y) => X <= x && x < X + Width && Y <= y && y < Y + Height; + + /// + /// Determines if the specified point is contained within the rectangular region defined by this + /// . + /// + /// The to test. + /// This method returns if the point represented by + /// is contained within this structure; otherwise . + public readonly bool Contains(Point pt) => Contains(pt.X, pt.Y); + + /// + /// Determines if the rectangular region represented by is entirely contained within the + /// rectangular region represented by this . + /// + /// The to test. + /// This method returns if the rectangular region represented by + /// is entirely contained within this structure; otherwise . + public readonly bool Contains(Rectangle rect) => + (X <= rect.X) && (rect.X + rect.Width <= X + Width) && + (Y <= rect.Y) && (rect.Y + rect.Height <= Y + Height); + + /// + /// Returns the hash code for this structure. + /// + /// An integer that represents the hash code for this rectangle. + public override readonly int GetHashCode() => HashCode.Combine(X, Y, Width, Height); + + /// + /// Inflates this by the specified amount. + /// + /// The amount to inflate this horizontally. + /// The amount to inflate this vertically. + public void Inflate(int width, int height) + { + unchecked + { + X -= width; + Y -= height; + + Width += 2 * width; + Height += 2 * height; + } + } + + /// + /// Creates a that is inflated by the specified amount. + /// + /// The with which to start. This rectangle is not modified. + /// The amount to inflate this horizontally. + /// The amount to inflate this vertically. + public static Rectangle Inflate(Rectangle rect, int x, int y) + { + Rectangle r = rect; + r.Inflate(x, y); + return r; + } + + /// + /// Creates a Rectangle that represents the intersection between this Rectangle and rect. + /// + /// The with which to intersect. + public void Intersect(Rectangle rect) + { + Rectangle result = Intersect(rect, this); + + X = result.X; + Y = result.Y; + Width = result.Width; + Height = result.Height; + } + + /// + /// Creates a rectangle that represents the intersection between a and b. If there is no intersection, an + /// empty rectangle is returned. + /// + /// A rectangle to intersect. + /// A rectangle to intersect. + /// A that represents the intersection of and . + public static Rectangle Intersect(Rectangle a, Rectangle b) + { + int x1 = Math.Max(a.X, b.X); + int x2 = Math.Min(a.X + a.Width, b.X + b.Width); + int y1 = Math.Max(a.Y, b.Y); + int y2 = Math.Min(a.Y + a.Height, b.Y + b.Height); + + return x2 >= x1 && y2 >= y1 ? new Rectangle(x1, y1, x2 - x1, y2 - y1) : Empty; + } + + /// + /// Determines if this rectangle intersects with rect. + /// + /// The rectangle to test. + /// This method returns if there is any intersection, otherwise . + public readonly bool IntersectsWith(Rectangle rect) => + (rect.X < X + Width) && (X < rect.X + rect.Width) && + (rect.Y < Y + Height) && (Y < rect.Y + rect.Height); + + /// + /// Creates a rectangle that represents the union between a and b. + /// + /// A rectangle to union. + /// A rectangle to union. + /// A structure that bounds the union of the two structures. + public static Rectangle Union(Rectangle a, Rectangle b) + { + int x1 = Math.Min(a.X, b.X); + int x2 = Math.Max(a.X + a.Width, b.X + b.Width); + int y1 = Math.Min(a.Y, b.Y); + int y2 = Math.Max(a.Y + a.Height, b.Y + b.Height); + + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// Amount to offset the location. + public void Offset(Point pos) => Offset(pos.X, pos.Y); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The horizontal offset. + /// The vertical offset. + public void Offset(int x, int y) + { + unchecked + { + X += x; + Y += y; + } + } + + /// + /// Converts the attributes of this to a human readable string. + /// + /// A string that contains the position, width, and height of this structure ¾ + /// for example, {X=20, Y=20, Width=100, Height=50}. + public override readonly string ToString() => $"{{X={X},Y={Y},Width={Width},Height={Height}}}"; + } +} +#endif \ No newline at end of file diff --git a/AdvancedSharpAdbClient/Properties/GlobalUsings.cs b/AdvancedSharpAdbClient/Properties/GlobalUsings.cs index 56cd0031..c4d2f724 100644 --- a/AdvancedSharpAdbClient/Properties/GlobalUsings.cs +++ b/AdvancedSharpAdbClient/Properties/GlobalUsings.cs @@ -1,4 +1,15 @@ -#if NET +#region AdvancedSharpAdbClient +global using AdvancedSharpAdbClient.DeviceCommands; +global using AdvancedSharpAdbClient.Exceptions; +global using AdvancedSharpAdbClient.Logs; +global using AdvancedSharpAdbClient.Models; +global using AdvancedSharpAdbClient.Models.DeviceCommands; +global using AdvancedSharpAdbClient.Polyfills; +global using AdvancedSharpAdbClient.Receivers; +global using AdvancedSharpAdbClient.Receivers.DeviceCommands; +#endregion + +#if NET global using System.Runtime.Versioning; #endif @@ -17,12 +28,17 @@ global using Buffer = System.Buffer; global using DateTime = System.DateTime; global using TimeSpan = System.TimeSpan; +#if HAS_DRAWING +global using Point = System.Drawing.Point; +#else +global using Point = AdvancedSharpAdbClient.Polyfills.Point; +#endif #endif #if WINDOWS10_0_17763_0_OR_GREATER -global using Windows.Foundation; global using Buffer = System.Buffer; global using DateTime = System.DateTime; +global using Point = System.Drawing.Point; global using TimeSpan = System.TimeSpan; #endif @@ -30,17 +46,15 @@ global using System.Threading.Tasks; #endif -#if HAS_LOGGER -global using Microsoft.Extensions.Logging; -global using Microsoft.Extensions.Logging.Abstractions; -#endif - #if HAS_BUFFERS global using System.Buffers; #endif #if HAS_DRAWING global using System.Drawing; +#endif + +#if HAS_IMAGING global using System.Drawing.Imaging; #endif diff --git a/AdvancedSharpAdbClient/Receivers/ConsoleOutputReceiver.cs b/AdvancedSharpAdbClient/Receivers/ConsoleOutputReceiver.cs index 5ad3702f..8d857f41 100644 --- a/AdvancedSharpAdbClient/Receivers/ConsoleOutputReceiver.cs +++ b/AdvancedSharpAdbClient/Receivers/ConsoleOutputReceiver.cs @@ -2,58 +2,35 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Receivers { /// /// Receives console output, and makes the console output available as a . To /// fetch the console output that was received, used the method. /// - public partial class ConsoleOutputReceiver : MultiLineReceiver + /// The logger to use when logging. + public partial class ConsoleOutputReceiver(ILogger? logger = null) : MultiLineReceiver { /// /// The default to use when parsing the output. /// protected const RegexOptions DefaultRegexOptions = RegexOptions.Singleline | RegexOptions.IgnoreCase; -#if HAS_LOGGER /// /// The logger to use when logging messages. /// - protected readonly ILogger logger; -#endif + protected readonly ILogger logger = logger ?? LoggerProvider.CreateLogger(); /// /// A which receives all output from the device. /// protected readonly StringBuilder output = new(); -#if !HAS_LOGGER -#pragma warning disable CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif - /// - /// Initializes a new instance of the class. - /// - /// The logger to use when logging. - public ConsoleOutputReceiver( -#if HAS_LOGGER - ILogger logger = null -#endif - ) - { -#if HAS_LOGGER - this.logger = logger ?? NullLogger.Instance; -#endif - } -#if !HAS_LOGGER -#pragma warning restore CS1572 // XML 注释中有 param 标记,但是没有该名称的参数 -#endif - /// /// Gets a that represents the current . /// @@ -70,35 +47,27 @@ public virtual void ThrowOnError(string line) { if (line.EndsWith(": not found")) { -#if HAS_LOGGER logger.LogWarning($"The remote execution returned: '{line}'"); -#endif throw new FileNotFoundException($"The remote execution returned: '{line}'"); } if (line.EndsWith("No such file or directory")) { -#if HAS_LOGGER logger.LogWarning($"The remote execution returned: {line}"); -#endif throw new FileNotFoundException($"The remote execution returned: '{line}'"); } // for "unknown options" if (line.Contains("Unknown option")) { -#if HAS_LOGGER logger.LogWarning($"The remote execution returned: {line}"); -#endif throw new UnknownOptionException($"The remote execution returned: '{line}'"); } // for "aborting" commands if (AbortingRegex().IsMatch(line)) { -#if HAS_LOGGER logger.LogWarning($"The remote execution returned: {line}"); -#endif throw new CommandAbortingException($"The remote execution returned: '{line}'"); } @@ -106,9 +75,7 @@ public virtual void ThrowOnError(string line) // cmd: applet not found if (AppletRegex().IsMatch(line)) { -#if HAS_LOGGER logger.LogWarning($"The remote execution returned: '{line}'"); -#endif throw new FileNotFoundException($"The remote execution returned: '{line}'"); } @@ -116,9 +83,7 @@ public virtual void ThrowOnError(string line) // workitem: 16822 if (DeniedRegex().IsMatch(line)) { -#if HAS_LOGGER logger.LogWarning($"The remote execution returned: '{line}'"); -#endif throw new PermissionDeniedException($"The remote execution returned: '{line}'"); } } @@ -132,16 +97,12 @@ protected override void ProcessNewLines(IEnumerable lines) { foreach (string line in lines) { - if (string.IsNullOrEmpty(line) || line.StartsWith("#") || line.StartsWith("$")) + if (string.IsNullOrEmpty(line) || line.StartsWith('#') || line.StartsWith('$')) { continue; } - output.AppendLine(line); - -#if HAS_LOGGER logger.LogDebug(line); -#endif } } diff --git a/AdvancedSharpAdbClient/Receivers/IShellOutputReceiver.cs b/AdvancedSharpAdbClient/Receivers/IShellOutputReceiver.cs index b98a1454..bd0f16a4 100644 --- a/AdvancedSharpAdbClient/Receivers/IShellOutputReceiver.cs +++ b/AdvancedSharpAdbClient/Receivers/IShellOutputReceiver.cs @@ -2,11 +2,13 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -namespace AdvancedSharpAdbClient +using System.Text; + +namespace AdvancedSharpAdbClient.Receivers { /// /// This interface contains various receivers that are able to parse Android console output. You can use - /// the receivers in combination with the + /// the receivers in combination with the /// method to capture the output of any Android command. /// public interface IShellOutputReceiver diff --git a/AdvancedSharpAdbClient/Receivers/MultilineReceiver.cs b/AdvancedSharpAdbClient/Receivers/MultilineReceiver.cs index 095a797f..f98c2765 100644 --- a/AdvancedSharpAdbClient/Receivers/MultilineReceiver.cs +++ b/AdvancedSharpAdbClient/Receivers/MultilineReceiver.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; -namespace AdvancedSharpAdbClient +namespace AdvancedSharpAdbClient.Receivers { /// /// A multiline receiver to receive and process shell output with multi lines. @@ -26,10 +26,8 @@ public abstract class MultiLineReceiver : IShellOutputReceiver /// Gets or sets a value indicating whether the receiver parses error messages. /// /// if this receiver parsers error messages; otherwise . - /// - /// The default value is . If set to , the - /// will detect common error messages and throw an exception. - /// + /// The default value is . If set to , the + /// will detect common error messages and throw an exception. public virtual bool ParsesErrors { get; protected set; } /// diff --git a/AdvancedSharpAdbClient/Receivers/NamespaceDoc.cs b/AdvancedSharpAdbClient/Receivers/NamespaceDoc.cs new file mode 100644 index 00000000..e332cc55 --- /dev/null +++ b/AdvancedSharpAdbClient/Receivers/NamespaceDoc.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. +// + +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text; + +namespace AdvancedSharpAdbClient.Receivers +{ + /// + /// The classes in this namespace provide receivers for . + /// + /// Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. + [CompilerGenerated] + [EditorBrowsable(EditorBrowsableState.Never)] + internal class NamespaceDoc : AdvancedSharpAdbClient.NamespaceDoc { } +} diff --git a/AdvancedSharpAdbClient/SyncService.Async.cs b/AdvancedSharpAdbClient/SyncService.Async.cs index d7fc5d85..5bae67d2 100644 --- a/AdvancedSharpAdbClient/SyncService.Async.cs +++ b/AdvancedSharpAdbClient/SyncService.Async.cs @@ -3,11 +3,10 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; @@ -19,42 +18,23 @@ public partial class SyncService public virtual async Task OpenAsync(CancellationToken cancellationToken = default) { // target a specific device - await Socket.SetDeviceAsync(Device, cancellationToken); + await Socket.SetDeviceAsync(Device, cancellationToken).ConfigureAwait(false); - await Socket.SendAdbRequestAsync("sync:", cancellationToken); - _ = await Socket.ReadAdbResponseAsync(cancellationToken); + await Socket.SendAdbRequestAsync("sync:", cancellationToken).ConfigureAwait(false); + _ = await Socket.ReadAdbResponseAsync(cancellationToken).ConfigureAwait(false); } - /// - /// Reopen this connection. - /// - /// A that enables to connection with the adb server. - /// A which can be used to cancel the asynchronous operation. - /// A which represents the asynchronous operation. - public virtual Task ReopenAsync(IAdbSocket socket, CancellationToken cancellationToken = default) + /// + public virtual async Task ReopenAsync(CancellationToken cancellationToken = default) { - if (Socket != null) - { - Socket.Dispose(); - Socket = null; - } - Socket = socket; - return OpenAsync(cancellationToken); + await Socket.ReconnectAsync(true, cancellationToken).ConfigureAwait(false); + await OpenAsync(cancellationToken).ConfigureAwait(false); } - /// - /// Reopen this connection. - /// - /// A connection to an adb server. - /// A which can be used to cancel the asynchronous operation. - /// A which represents the asynchronous operation. - public Task ReopenAsync(IAdbClient client, CancellationToken cancellationToken = default) => ReopenAsync(Factories.AdbSocketFactory(client.EndPoint), cancellationToken); - /// - public virtual async Task PushAsync(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress, CancellationToken cancellationToken = default) + public virtual async Task PushAsync(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress? progress = null, CancellationToken cancellationToken = default) { ExceptionExtensions.ThrowIfNull(stream); - ExceptionExtensions.ThrowIfNull(remotePath); if (remotePath.Length > MaxPathLength) @@ -62,7 +42,7 @@ public virtual async Task PushAsync(Stream stream, string remotePath, int permis throw new ArgumentOutOfRangeException(nameof(remotePath), $"The remote path {remotePath} exceeds the maximum path size {MaxPathLength}"); } - await Socket.SendSyncRequestAsync(SyncCommand.SEND, remotePath, permissions, cancellationToken); + await Socket.SendSyncRequestAsync(SyncCommand.SEND, remotePath, permissions, cancellationToken).ConfigureAwait(false); // create the buffer used to read. // we read max SYNC_DATA_MAX. @@ -90,7 +70,12 @@ public virtual async Task PushAsync(Stream stream, string remotePath, int permis cancellationToken.ThrowIfCancellationRequested(); // read up to SYNC_DATA_MAX - int read = stream.Read(buffer, headerSize, maxDataSize); + int read = +#if HAS_BUFFERS + await stream.ReadAsync(buffer.AsMemory(headerSize, maxDataSize), cancellationToken).ConfigureAwait(false); +#else + await stream.ReadAsync(buffer, headerSize, maxDataSize, cancellationToken).ConfigureAwait(false); +#endif totalBytesRead += read; if (read == 0) @@ -111,7 +96,11 @@ public virtual async Task PushAsync(Stream stream, string remotePath, int permis Buffer.BlockCopy(lengthBytes, 0, buffer, startPosition + dataBytes.Length, lengthBytes.Length); // now send the data to the device - await Socket.SendAsync(buffer, startPosition, read + dataBytes.Length + lengthBytes.Length, cancellationToken); +#if HAS_BUFFERS + await Socket.SendAsync(buffer.AsMemory(startPosition, read + dataBytes.Length + lengthBytes.Length), cancellationToken).ConfigureAwait(false); +#else + await Socket.SendAsync(buffer, startPosition, read + dataBytes.Length + lengthBytes.Length, cancellationToken).ConfigureAwait(false); +#endif SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(totalBytesRead, totalBytesToProcess)); @@ -124,15 +113,15 @@ public virtual async Task PushAsync(Stream stream, string remotePath, int permis // create the DONE message int time = (int)timestamp.ToUnixTimeSeconds(); - await Socket.SendSyncRequestAsync(SyncCommand.DONE, time, cancellationToken); + await Socket.SendSyncRequestAsync(SyncCommand.DONE, time, cancellationToken).ConfigureAwait(false); // read the result, in a byte array containing 2 int // (id, size) - SyncCommand result = await Socket.ReadSyncResponseAsync(cancellationToken); + SyncCommand result = await Socket.ReadSyncResponseAsync(cancellationToken).ConfigureAwait(false); if (result == SyncCommand.FAIL) { - string message = await Socket.ReadSyncStringAsync(cancellationToken); + string message = await Socket.ReadSyncStringAsync(cancellationToken).ConfigureAwait(false); throw new AdbException(message); } @@ -143,24 +132,23 @@ public virtual async Task PushAsync(Stream stream, string remotePath, int permis } /// - public virtual async Task PullAsync(string remoteFilePath, Stream stream, IProgress progress, CancellationToken cancellationToken = default) + public virtual async Task PullAsync(string remoteFilePath, Stream stream, IProgress? progress = null, CancellationToken cancellationToken = default) { ExceptionExtensions.ThrowIfNull(remoteFilePath); - ExceptionExtensions.ThrowIfNull(stream); // Get file information, including the file size, used to calculate the total amount of bytes to receive. - FileStatistics stat = await StatAsync(remoteFilePath, cancellationToken); + FileStatistics stat = await StatAsync(remoteFilePath, cancellationToken).ConfigureAwait(false); long totalBytesToProcess = stat.Size; long totalBytesRead = 0; byte[] buffer = new byte[MaxBufferSize]; - await Socket.SendSyncRequestAsync(SyncCommand.RECV, remoteFilePath, cancellationToken); + await Socket.SendSyncRequestAsync(SyncCommand.RECV, remoteFilePath, cancellationToken).ConfigureAwait(false); while (true) { - SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken); + SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (response == SyncCommand.DONE) @@ -169,7 +157,7 @@ public virtual async Task PullAsync(string remoteFilePath, Stream stream, IProgr } else if (response == SyncCommand.FAIL) { - string message = await Socket.ReadSyncStringAsync(cancellationToken); + string message = await Socket.ReadSyncStringAsync(cancellationToken).ConfigureAwait(false); throw new AdbException($"Failed to pull '{remoteFilePath}'. {message}"); } else if (response != SyncCommand.DATA) @@ -179,14 +167,19 @@ public virtual async Task PullAsync(string remoteFilePath, Stream stream, IProgr // The first 4 bytes contain the length of the data packet byte[] reply = new byte[4]; - _ = await Socket.ReadAsync(reply, cancellationToken); + _ = await Socket.ReadAsync(reply, cancellationToken).ConfigureAwait(false); if (!BitConverter.IsLittleEndian) { Array.Reverse(reply); } - int size = BitConverter.ToInt32(reply, 0); + int size = +#if HAS_BUFFERS + BitConverter.ToInt32(reply); +#else + BitConverter.ToInt32(reply, 0); +#endif if (size > MaxBufferSize) { @@ -194,13 +187,12 @@ public virtual async Task PullAsync(string remoteFilePath, Stream stream, IProgr } // now read the length we received - await Socket.ReadAsync(buffer, size, cancellationToken); -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await stream.WriteAsync(buffer.AsMemory(0, size), cancellationToken); -#elif !NET35 - await stream.WriteAsync(buffer, 0, size, cancellationToken); +#if HAS_BUFFERS + await Socket.ReadAsync(buffer.AsMemory(0, size), cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, size), cancellationToken).ConfigureAwait(false); #else - await Utilities.Run(() => stream.Write(buffer, 0, size)); + await Socket.ReadAsync(buffer, size, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer, size, cancellationToken).ConfigureAwait(false); #endif totalBytesRead += size; @@ -218,49 +210,61 @@ public virtual async Task PullAsync(string remoteFilePath, Stream stream, IProgr public virtual async Task StatAsync(string remotePath, CancellationToken cancellationToken = default) { // create the stat request message. - await Socket.SendSyncRequestAsync(SyncCommand.STAT, remotePath, cancellationToken); + await Socket.SendSyncRequestAsync(SyncCommand.STAT, remotePath, cancellationToken).ConfigureAwait(false); - if (await Socket.ReadSyncResponseAsync(cancellationToken) != SyncCommand.STAT) + SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken).ConfigureAwait(false); + if (response != SyncCommand.STAT) { - throw new AdbException($"The server returned an invalid sync response."); + throw new AdbException($"The server returned an invalid sync response {response}."); } // read the result, in a byte array containing 3 int // (mode, size, time) - FileStatistics value = new() - { - Path = remotePath - }; - - await ReadStatisticsAsync(value, cancellationToken); + FileStatistics value = await ReadStatisticsAsync(cancellationToken).ConfigureAwait(false); + value.Path = remotePath; return value; } /// - public virtual async Task> GetDirectoryListingAsync(string remotePath, CancellationToken cancellationToken = default) + public virtual async Task> GetDirectoryListingAsync(string remotePath, CancellationToken cancellationToken = default) { - Collection value = new(); + bool isLocked = false; + + start: + List value = []; // create the stat request message. - await Socket.SendSyncRequestAsync(SyncCommand.LIST, remotePath, cancellationToken); + await Socket.SendSyncRequestAsync(SyncCommand.LIST, remotePath, cancellationToken).ConfigureAwait(false); while (true) { - SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken); + SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken).ConfigureAwait(false); - if (response == SyncCommand.DONE) + if (response == 0) + { + if (isLocked) + { + throw new AdbException("The server returned an empty sync response."); + } + else + { + Reopen(); + isLocked = true; + goto start; + } + } + else if (response == SyncCommand.DONE) { break; } else if (response != SyncCommand.DENT) { - throw new AdbException($"The server returned an invalid sync response."); + throw new AdbException($"The server returned an invalid sync response {response}."); } - FileStatistics entry = new(); - await ReadStatisticsAsync(entry, cancellationToken); - entry.Path = await Socket.ReadSyncStringAsync(cancellationToken); + FileStatistics entry = await ReadStatisticsAsync(cancellationToken).ConfigureAwait(false); + entry.Path = await Socket.ReadSyncStringAsync(cancellationToken).ConfigureAwait(false); value.Add(entry); } @@ -272,46 +276,67 @@ public virtual async Task> GetDirectoryListingAsync( /// public virtual async IAsyncEnumerable GetDirectoryAsyncListing(string remotePath, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + bool isLocked = false; + + start: // create the stat request message. - await Socket.SendSyncRequestAsync(SyncCommand.LIST, remotePath, cancellationToken); + await Socket.SendSyncRequestAsync(SyncCommand.LIST, remotePath, cancellationToken).ConfigureAwait(false); while (true) { - SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken); + SyncCommand response = await Socket.ReadSyncResponseAsync(cancellationToken).ConfigureAwait(false); - if (response == SyncCommand.DONE) + if (response == 0) + { + if (isLocked) + { + throw new AdbException("The server returned an empty sync response."); + } + else + { + Reopen(); + isLocked = true; + goto start; + } + } + else if (response == SyncCommand.DONE) { break; } else if (response != SyncCommand.DENT) { - throw new AdbException($"The server returned an invalid sync response."); + throw new AdbException($"The server returned an invalid sync response {response}."); } - FileStatistics entry = new(); - await ReadStatisticsAsync(entry, cancellationToken); - entry.Path = await Socket.ReadSyncStringAsync(cancellationToken); + FileStatistics entry = await ReadStatisticsAsync(cancellationToken).ConfigureAwait(false); + entry.Path = await Socket.ReadSyncStringAsync(cancellationToken).ConfigureAwait(false); yield return entry; + isLocked = true; } } #endif - private async Task ReadStatisticsAsync(FileStatistics value, CancellationToken cancellationToken = default) + /// + /// Reads the statistics of a file from the socket. + /// + /// A that can be used to cancel the task. + /// A which return a object that contains information about the file. + protected async Task ReadStatisticsAsync(CancellationToken cancellationToken = default) { byte[] statResult = new byte[12]; - _ = await Socket.ReadAsync(statResult, cancellationToken); + _ = await Socket.ReadAsync(statResult, cancellationToken).ConfigureAwait(false); + + int index = 0; - if (!BitConverter.IsLittleEndian) + return new FileStatistics { - Array.Reverse(statResult, 0, 4); - Array.Reverse(statResult, 4, 4); - Array.Reverse(statResult, 8, 4); - } + FileType = (UnixFileType)ReadInt32(in statResult), + Size = ReadInt32(in statResult), + Time = DateTimeExtensions.FromUnixTimeSeconds(ReadInt32(in statResult)) + }; - value.FileMode = (UnixFileMode)BitConverter.ToInt32(statResult, 0); - value.Size = BitConverter.ToInt32(statResult, 4); - value.Time = Utilities.FromUnixTimeSeconds(BitConverter.ToInt32(statResult, 8)); + int ReadInt32(in byte[] data) => data[index++] | (data[index++] << 8) | (data[index++] << 16) | (data[index++] << 24); } } } diff --git a/AdvancedSharpAdbClient/SyncService.cs b/AdvancedSharpAdbClient/SyncService.cs index d29577ab..e8e118d2 100644 --- a/AdvancedSharpAdbClient/SyncService.cs +++ b/AdvancedSharpAdbClient/SyncService.cs @@ -2,11 +2,10 @@ // Copyright (c) The Android Open Source Project, Ryan Conrad, Quamotion, yungd1plomat, wherewhere. All rights reserved. // -using AdvancedSharpAdbClient.Exceptions; using System; using System.Collections.Generic; using System.IO; -using System.Threading; +using System.Net; namespace AdvancedSharpAdbClient { @@ -48,14 +47,34 @@ public partial class SyncService : ISyncService protected const int MaxPathLength = 1024; /// - public event EventHandler SyncProgressChanged; + public event EventHandler? SyncProgressChanged; + + /// + /// Initializes a new instance of the class. + /// + /// The device on which to interact with the files. + public SyncService(DeviceData device) + : this(Factories.AdbSocketFactory(new IPEndPoint(IPAddress.Loopback, AdbClient.AdbServerPort)), device) + { + } /// /// Initializes a new instance of the class. /// /// A connection to an adb server. /// The device on which to interact with the files. - public SyncService(IAdbClient client, DeviceData device) : this(Factories.AdbSocketFactory(client.EndPoint), device) + public SyncService(IAdbClient client, DeviceData device) + : this(Factories.AdbSocketFactory(client.EndPoint), device) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The at which the adb server is listening. + /// The device on which to interact with the files. + public SyncService(EndPoint endPoint, DeviceData device) + : this(Factories.AdbSocketFactory(endPoint), device) { } @@ -80,12 +99,12 @@ public SyncService(IAdbSocket socket, DeviceData device) /// /// Gets the device on which the file operations are being executed. /// - public DeviceData Device { get; private set; } + public DeviceData Device { get; } /// /// Gets the that enables the connection with the adb server. /// - public IAdbSocket Socket { get; private set; } + public IAdbSocket Socket { get; protected set; } /// public bool IsOpen => Socket != null && Socket.Connected; @@ -100,36 +119,17 @@ public virtual void Open() _ = Socket.ReadAdbResponse(); } - /// - /// Reopen this connection. - /// - /// A that enables to connection with the adb server. - public virtual void Reopen(IAdbSocket socket) + /// + public virtual void Reopen() { - if (Socket != null) - { - Socket.Dispose(); - Socket = null; - } - Socket = socket; + Socket.Reconnect(true); Open(); } - /// - /// Reopen this connection. - /// - /// A connection to an adb server. - public void Reopen(IAdbClient client) => Reopen(Factories.AdbSocketFactory(client.EndPoint)); - /// - public virtual void Push(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress progress -#if HAS_TASK - , CancellationToken cancellationToken = default -#endif - ) + public virtual void Push(Stream stream, string remotePath, int permissions, DateTimeOffset timestamp, IProgress? progress = null, in bool isCancelled = false) { ExceptionExtensions.ThrowIfNull(stream); - ExceptionExtensions.ThrowIfNull(remotePath); if (remotePath.Length > MaxPathLength) @@ -159,15 +159,14 @@ public virtual void Push(Stream stream, string remotePath, int permissions, Date long totalBytesRead = 0; // look while there is something to read - while (true) + while (!isCancelled) { -#if HAS_TASK - // check if we're canceled - cancellationToken.ThrowIfCancellationRequested(); -#endif - // read up to SYNC_DATA_MAX +#if HAS_BUFFERS + int read = stream.Read(buffer.AsSpan(headerSize, maxDataSize)); +#else int read = stream.Read(buffer, headerSize, maxDataSize); +#endif totalBytesRead += read; if (read == 0) @@ -188,7 +187,11 @@ public virtual void Push(Stream stream, string remotePath, int permissions, Date Buffer.BlockCopy(lengthBytes, 0, buffer, startPosition + dataBytes.Length, lengthBytes.Length); // now send the data to the device +#if HAS_BUFFERS + Socket.Send(buffer.AsSpan(startPosition, read + dataBytes.Length + lengthBytes.Length)); +#else Socket.Send(buffer, startPosition, read + dataBytes.Length + lengthBytes.Length); +#endif SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(totalBytesRead, totalBytesToProcess)); @@ -220,14 +223,9 @@ public virtual void Push(Stream stream, string remotePath, int permissions, Date } /// - public virtual void Pull(string remoteFilePath, Stream stream, IProgress progress -#if HAS_TASK - , CancellationToken cancellationToken = default -#endif - ) + public virtual void Pull(string remoteFilePath, Stream stream, IProgress? progress = null, in bool isCancelled = false) { ExceptionExtensions.ThrowIfNull(remoteFilePath); - ExceptionExtensions.ThrowIfNull(stream); // Get file information, including the file size, used to calculate the total amount of bytes to receive. @@ -239,12 +237,10 @@ public virtual void Pull(string remoteFilePath, Stream stream, IProgress pr Socket.SendSyncRequest(SyncCommand.RECV, remoteFilePath); - while (true) + while (!isCancelled) { SyncCommand response = Socket.ReadSyncResponse(); -#if HAS_TASK - cancellationToken.ThrowIfCancellationRequested(); -#endif + if (response == SyncCommand.DONE) { break; @@ -268,7 +264,12 @@ public virtual void Pull(string remoteFilePath, Stream stream, IProgress pr Array.Reverse(reply); } - int size = BitConverter.ToInt32(reply, 0); + int size = +#if HAS_BUFFERS + BitConverter.ToInt32(reply); +#else + BitConverter.ToInt32(reply, 0); +#endif if (size > MaxBufferSize) { @@ -276,8 +277,13 @@ public virtual void Pull(string remoteFilePath, Stream stream, IProgress pr } // now read the length we received +#if HAS_BUFFERS + _ = Socket.Read(buffer.AsSpan(0, size)); + stream.Write(buffer.AsSpan(0, size)); +#else _ = Socket.Read(buffer, size); - stream.Write(buffer, 0, size); + stream.Write(buffer, size); +#endif totalBytesRead += size; SyncProgressChanged?.Invoke(this, new SyncProgressChangedEventArgs(totalBytesRead, totalBytesToProcess)); @@ -296,19 +302,16 @@ public virtual FileStatistics Stat(string remotePath) // create the stat request message. Socket.SendSyncRequest(SyncCommand.STAT, remotePath); - if (Socket.ReadSyncResponse() != SyncCommand.STAT) + SyncCommand response = Socket.ReadSyncResponse(); + if (response != SyncCommand.STAT) { - throw new AdbException($"The server returned an invalid sync response."); + throw new AdbException($"The server returned an invalid sync response {response}."); } // read the result, in a byte array containing 3 int // (mode, size, time) - FileStatistics value = new() - { - Path = remotePath - }; - - ReadStatistics(value); + FileStatistics value = ReadStatistics(); + value.Path = remotePath; return value; } @@ -316,6 +319,9 @@ public virtual FileStatistics Stat(string remotePath) /// public virtual IEnumerable GetDirectoryListing(string remotePath) { + bool isLocked = false; + + start: // create the stat request message. Socket.SendSyncRequest(SyncCommand.LIST, remotePath); @@ -323,20 +329,33 @@ public virtual IEnumerable GetDirectoryListing(string remotePath { SyncCommand response = Socket.ReadSyncResponse(); - if (response == SyncCommand.DONE) + if (response == 0) + { + if (isLocked) + { + throw new AdbException("The server returned an empty sync response."); + } + else + { + Reopen(); + isLocked = true; + goto start; + } + } + else if (response == SyncCommand.DONE) { break; } else if (response != SyncCommand.DENT) { - throw new AdbException($"The server returned an invalid sync response."); + throw new AdbException($"The server returned an invalid sync response {response}."); } - FileStatistics entry = new(); - ReadStatistics(entry); + FileStatistics entry = ReadStatistics(); entry.Path = Socket.ReadSyncString(); yield return entry; + isLocked = true; } } @@ -350,7 +369,7 @@ protected virtual void Dispose(bool disposing) if (Socket != null) { Socket.Dispose(); - Socket = null; + Socket = null!; } } } @@ -362,21 +381,25 @@ public void Dispose() GC.SuppressFinalize(this); } - private void ReadStatistics(FileStatistics value) + /// + /// Reads the statistics of a file from the socket. + /// + /// A object that contains information about the file. + protected FileStatistics ReadStatistics() { byte[] statResult = new byte[12]; _ = Socket.Read(statResult); - if (!BitConverter.IsLittleEndian) + int index = 0; + + return new FileStatistics { - Array.Reverse(statResult, 0, 4); - Array.Reverse(statResult, 4, 4); - Array.Reverse(statResult, 8, 4); - } + FileType = (UnixFileType)ReadInt32(in statResult), + Size = ReadInt32(in statResult), + Time = DateTimeExtensions.FromUnixTimeSeconds(ReadInt32(in statResult)) + }; - value.FileMode = (UnixFileMode)BitConverter.ToInt32(statResult, 0); - value.Size = BitConverter.ToInt32(statResult, 4); - value.Time = Utilities.FromUnixTimeSeconds(BitConverter.ToInt32(statResult, 8)); + int ReadInt32(in byte[] data) => data[index++] | (data[index++] << 8) | (data[index++] << 16) | (data[index++] << 24); } } } diff --git a/AdvancedSharpAdbClient/TcpSocket.Async.cs b/AdvancedSharpAdbClient/TcpSocket.Async.cs index d4387163..13452d07 100644 --- a/AdvancedSharpAdbClient/TcpSocket.Async.cs +++ b/AdvancedSharpAdbClient/TcpSocket.Async.cs @@ -4,6 +4,8 @@ // using System; +using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Sockets; using System.Threading; @@ -11,24 +13,113 @@ namespace AdvancedSharpAdbClient { public partial class TcpSocket { -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#if NET6_0_OR_GREATER /// - public virtual async Task SendAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => - await socket.SendAsync(buffer.AsMemory().Slice(offset, size), socketFlags, cancellationToken); + [MemberNotNull(nameof(EndPoint))] + public virtual async ValueTask ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken = default) + { + if (endPoint is not (IPEndPoint or DnsEndPoint)) + { + throw new NotSupportedException("Only TCP endpoints are supported"); + } + + EndPoint = endPoint; + await Socket.ConnectAsync(endPoint, cancellationToken).ConfigureAwait(false); + Socket.Blocking = true; + } + + /// + public virtual ValueTask ReconnectAsync(bool isForce, CancellationToken cancellationToken = default) + { + if (isForce || !Socket.Connected) + { + Socket.Dispose(); + Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + return ConnectAsync(EndPoint!, cancellationToken); + } + else + { + // Already connected - nothing to do. + return ValueTask.CompletedTask; + } + } #else /// - public virtual async Task SendAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => - await Utilities.Run(() => Send(buffer, offset, size, socketFlags), cancellationToken); + [MemberNotNull(nameof(EndPoint))] + public virtual async Task ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken = default) + { + if (endPoint is not (IPEndPoint or DnsEndPoint)) + { + throw new NotSupportedException("Only TCP endpoints are supported"); + } + + using (CancellationTokenRegistration cancellationTokenRegistration = cancellationToken.Register(Socket.Close)) + { + EndPoint = endPoint; + await Extensions.Yield(); + Socket.Connect(endPoint); + } + Socket.Blocking = true; + } + + /// + public virtual Task ReconnectAsync(bool isForce, CancellationToken cancellationToken = default) + { + if (isForce || !Socket.Connected) + { + Socket.Dispose(); + Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + return ConnectAsync(EndPoint!, cancellationToken); + } + else + { + // Already connected - nothing to do. + return Extensions.CompletedTask; + } + } #endif -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER /// - public virtual async Task ReceiveAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => - await socket.ReceiveAsync(buffer.AsMemory().Slice(offset, size), socketFlags, cancellationToken); + public +#if NET6_0_OR_GREATER + ValueTask #else + Task +#endif + ReconnectAsync(CancellationToken cancellationToken = default) => ReconnectAsync(false, cancellationToken); + + /// + public virtual Task SendAsync(byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.SendAsync(buffer, size, socketFlags, cancellationToken); + + /// + public virtual Task SendAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.SendAsync(buffer, offset, size, socketFlags, cancellationToken); + + /// + public virtual Task ReceiveAsync(byte[] buffer, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.ReceiveAsync(buffer, size, socketFlags, cancellationToken); + /// public virtual Task ReceiveAsync(byte[] buffer, int offset, int size, SocketFlags socketFlags, CancellationToken cancellationToken = default) => - socket.ReceiveAsync(buffer, offset, size, socketFlags, cancellationToken); + Socket.ReceiveAsync(buffer, offset, size, socketFlags, cancellationToken); + +#if HAS_BUFFERS + /// + public ValueTask SendAsync(ReadOnlyMemory buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.SendAsync(buffer, socketFlags, cancellationToken); + + /// + public ValueTask ReceiveAsync(Memory buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.ReceiveAsync(buffer, socketFlags, cancellationToken); +#else + /// + public virtual Task SendAsync(byte[] buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.SendAsync(buffer, socketFlags, cancellationToken); + + /// + public virtual Task ReceiveAsync(byte[] buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default) => + Socket.ReceiveAsync(buffer, socketFlags, cancellationToken); #endif } } diff --git a/AdvancedSharpAdbClient/TcpSocket.cs b/AdvancedSharpAdbClient/TcpSocket.cs index 129a74ca..fdcf020d 100644 --- a/AdvancedSharpAdbClient/TcpSocket.cs +++ b/AdvancedSharpAdbClient/TcpSocket.cs @@ -3,6 +3,7 @@ // using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; using System.Net.Sockets; @@ -10,36 +11,37 @@ namespace AdvancedSharpAdbClient { /// - /// Implements the interface using the standard class. + /// Implements the interface using the standard class. /// public partial class TcpSocket : ITcpSocket { /// - /// The underlying socket that manages the connection. + /// Initializes a new instance of the class. /// - protected Socket socket; + public TcpSocket() => Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); /// - /// The at which the socket is listening. + /// The underlying socket that manages the connection. /// - protected EndPoint endPoint; + public Socket Socket { get; protected set; } /// - /// Initializes a new instance of the class. + /// The at which the socket is listening. /// - public TcpSocket() => socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + public EndPoint? EndPoint { get; protected set; } /// - public bool Connected => socket.Connected; + public bool Connected => Socket.Connected; /// public int ReceiveBufferSize { - get => socket.ReceiveBufferSize; - set => socket.ReceiveBufferSize = value; + get => Socket.ReceiveBufferSize; + set => Socket.ReceiveBufferSize = value; } /// + [MemberNotNull(nameof(EndPoint))] public virtual void Connect(EndPoint endPoint) { if (endPoint is not (IPEndPoint or DnsEndPoint)) @@ -47,22 +49,25 @@ public virtual void Connect(EndPoint endPoint) throw new NotSupportedException("Only TCP endpoints are supported"); } - socket.Connect(endPoint); - socket.Blocking = true; - this.endPoint = endPoint; + EndPoint = endPoint; + Socket.Connect(endPoint); + Socket.Blocking = true; } /// - public virtual void Reconnect() + public virtual void Reconnect(bool isForce = false) { - if (socket.Connected) + if (isForce || !Socket.Connected) + { + Socket.Dispose(); + Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + Connect(EndPoint!); + } + else { // Already connected - nothing to do. return; } - - socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - Connect(endPoint); } /// @@ -70,7 +75,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - socket.Dispose(); + Socket.Dispose(); } } @@ -81,15 +86,44 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + public virtual void Close() => Socket.Close(); + + /// + public virtual int Send(byte[] buffer, int size, SocketFlags socketFlags) => + Socket.Send(buffer, size, socketFlags); + /// public virtual int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags) => - socket.Send(buffer, offset, size, socketFlags); + Socket.Send(buffer, offset, size, socketFlags); /// public virtual int Receive(byte[] buffer, int size, SocketFlags socketFlags) => - socket.Receive(buffer, size, socketFlags); + Socket.Receive(buffer, size, socketFlags); + + /// + public virtual int Receive(byte[] buffer, int offset, int size, SocketFlags socketFlags) => + Socket.Receive(buffer, offset, size, socketFlags); + +#if HAS_BUFFERS + /// + public virtual int Send(ReadOnlySpan buffer, SocketFlags socketFlags) => + Socket.Send(buffer, socketFlags); + + /// + public virtual int Receive(Span buffer, SocketFlags socketFlags) => + Socket.Receive(buffer, socketFlags); +#else + /// + public virtual int Send(byte[] buffer, SocketFlags socketFlags) => + Socket.Send(buffer, socketFlags); + + /// + public virtual int Receive(byte[] buffer, SocketFlags socketFlags) => + Socket.Receive(buffer, socketFlags); +#endif /// - public Stream GetStream() => new NetworkStream(socket); + public virtual Stream GetStream() => new NetworkStream(Socket); } } diff --git a/Directory.Build.props b/Directory.Build.props index f1525f86..31af2be7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,19 +12,19 @@ True latest Icon.png - https://raw.githubusercontent.com/yungd1plomat/AdvancedSharpAdbClient/main/logo.png + https://raw.githubusercontent.com/SharpAdb/AdvancedSharpAdbClient/main/logo.png Apache-2.0 - https://github.com/yungd1plomat/AdvancedSharpAdbClient + https://github.com/SharpAdb/AdvancedSharpAdbClient True - https://github.com/yungd1plomat/AdvancedSharpAdbClient/releases + https://github.com/SharpAdb/AdvancedSharpAdbClient/releases Android;ADB;Communicate;UWP;Xamarin;MAUI;WinUI;Mono;Unity;SharpAdbClient;AdvancedSharpAdbClient git AdvancedSharpAdbClient: A .NET client for the Android Debug Bridge (adb) True - https://github.com/yungd1plomat/AdvancedSharpAdbClient + https://github.com/SharpAdb/AdvancedSharpAdbClient snupkg .NET client for adb, Android Debug Bridge (AdvancedSharpAdbClient) - 2.5.8 + 3.0.9 @@ -35,20 +35,24 @@ $(MSBuildProjectName.Contains('.Test')) - + .NETCore,Version=v5.0 - - True + + 10.0 + + + + $(DefineConstants);UAP10_0_15138_0 + 10.0.15138.0 False en-US - $(DefineConstants);NETCORE;NETCORE_5_0;NETFX_CORE;WINDOWS_UWP + $(DefineConstants);NETCORE;NETCORE_5_0;NETFX_CORE;WINDOWS_UWP;UAP10_0 $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets UAP,Version=v10.0 .NETCore diff --git a/Directory.Build.targets b/Directory.Build.targets index 94686618..90a9371a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -6,7 +6,7 @@ - + diff --git a/README.md b/README.md index 7568c077..bcf64b1a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ | Issues | License | NuGet | |--------|---------|-------| -[![Github Issues](https://img.shields.io/github/issues/yungd1plomat/AdvancedSharpAdbClient)](https://github.com/yungd1plomat/AdvancedSharpAdbClient/issues)|[![License](https://img.shields.io/github/license/yungd1plomat/AdvancedSharpAdbClient)](https://github.com/yungd1plomat/AdvancedSharpAdbClient/blob/main/LICENSE)|[![NuGet Status](https://img.shields.io/nuget/dt/AdvancedSharpAdbClient.svg?style=flat)](https://www.nuget.org/packages/AdvancedSharpAdbClient/) +[![Github Issues](https://img.shields.io/github/issues/SharpAdb/AdvancedSharpAdbClient)](https://github.com/SharpAdb/AdvancedSharpAdbClient/issues)|[![License](https://img.shields.io/github/license/SharpAdb/AdvancedSharpAdbClient)](https://github.com/SharpAdb/AdvancedSharpAdbClient/blob/main/LICENSE)|[![NuGet Status](https://img.shields.io/nuget/dt/AdvancedSharpAdbClient.svg?style=flat)](https://www.nuget.org/packages/AdvancedSharpAdbClient/) # A .NET client for adb, the Android Debug Bridge (AdvancedSharpAdbClient) @@ -17,7 +17,6 @@ Added important features. - .NET Framework 4.0 (Need [Microsoft.Bcl.Async](https://www.nuget.org/packages/Microsoft.Bcl.Async)) - .NET Framework 4.5.2 - .NET Framework 4.6.2 -- .NET Framework 4.7.2 - .NET Framework 4.8.1 - .NET Standard 1.3 - .NET Standard 2.0 @@ -88,20 +87,20 @@ static void Main(string[] args) client = new AdbClient(); client.Connect("127.0.0.1:62001"); device = client.GetDevices().FirstOrDefault(); - Element el = client.FindElement(device, "//node[@text='Login']"); + Element element = client.FindElement(device, "//node[@text='Login']"); } ``` You can also specify the waiting time for the element ```cs -Element el = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(5)); +Element element = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(5)); ``` And you can find several elements ```cs -Element[] els = client.FindElements(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(5)); +Element[] element = client.FindElements(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(5)); ``` ### Getting element attributes @@ -111,9 +110,9 @@ You can get all element attributes static void Main(string[] args) { ... - Element el = client.FindElement(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(3)); - string eltext = el.attributes["text"]; - string bounds = el.attributes["bounds"]; + Element element = client.FindElement(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(3)); + string eltext = element.Attributes["text"]; + string bounds = element.Attributes["bounds"]; ... } ``` @@ -137,8 +136,8 @@ Or on the element(need xpath) static void Main(string[] args) { ... - Element el = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(3)); - el.Click();// Click on element by xpath //node[@text='Login'] + Element element = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(3)); + element.Click(); // Click on element by xpath //node[@text='Login'] ... } ``` @@ -148,7 +147,7 @@ The Click() method throw ElementNotFoundException if the element is not found ```cs try { - el.Click(); + element.Click(); } catch (Exception ex) { @@ -188,7 +187,7 @@ The Swipe() method throw ElementNotFoundException if the element is not found ```cs try { - client.Swipe(device, 0x2232323, 0x954,0x9128,0x11111, 200); + client.Swipe(device, 0x2232323, 0x954, 0x9128, 0x11111, 200); ... client.Swipe(device, first, second, 200); } @@ -298,9 +297,9 @@ catch (Exception ex) static void Main(string[] args) { ... - client.BackBtn(device); // Click Back button + client.ClickBackButton(device); // Click Back button ... - client.HomeBtn(device); // Click Home button + client.ClickHomeButton(device); // Click Home button ... } ``` @@ -314,7 +313,7 @@ static void Main(string[] args) { ... PackageManager manager = new PackageManager(client, device); - manager.InstallPackage(@"C:\Users\me\Documents\mypackage.apk", reinstall: false); + manager.InstallPackage(@"C:\Users\me\Documents\mypackage.apk"); manager.UninstallPackage("com.android.app"); ... } @@ -326,7 +325,11 @@ Or you can use AdbClient.Install static void Main(string[] args) { ... - client.Install(device, File.OpenRead("Application.apk")); + using (FileStream stream = File.OpenRead("Application.apk")) + { + client.Install(device, stream); + client.Uninstall(device, "com.android.app"); + } ... } ``` @@ -337,8 +340,8 @@ static void Main(string[] args) { ... PackageManager manager = new PackageManager(client, device); - manager.InstallMultiplePackage(@"C:\Users\me\Documents\base.apk", new string[] { @"C:\Users\me\Documents\split_1.apk", @"C:\Users\me\Documents\split_2.apk" }, reinstall: false); // Install split app whith base app - manager.InstallMultiplePackage(new string[] { @"C:\Users\me\Documents\split_3.apk", @"C:\Users\me\Documents\split_4.apk" }, "com.android.app", reinstall: false); // Add split app to base app which packagename is 'com.android.app' + manager.InstallMultiplePackage(@"C:\Users\me\Documents\base.apk", new[] { @"C:\Users\me\Documents\split_1.apk", @"C:\Users\me\Documents\split_2.apk" }); // Install split app whith base app + manager.InstallMultiplePackage(new[] { @"C:\Users\me\Documents\split_3.apk", @"C:\Users\me\Documents\split_4.apk" }, "com.android.app"); // Add split app to base app which packagename is 'com.android.app' ... } ``` @@ -349,8 +352,8 @@ Or you can use AdbClient.InstallMultiple static void Main(string[] args) { ... - client.InstallMultiple(device, File.OpenRead("base.apk"), new Stream[] { File.OpenRead("split_1.apk"), File.OpenRead("split_2.apk") }); // Install split app whith base app - client.InstallMultiple(device, new Stream[] { File.OpenRead("split_3.apk"), File.OpenRead("split_4.apk") }, "com.android.app"); // Add split app to base app which packagename is 'com.android.app' + client.InstallMultiple(device, File.OpenRead("base.apk"), new[] { File.OpenRead("split_1.apk"), File.OpenRead("split_2.apk") }); // Install split app whith base app + client.InstallMultiple(device, new[] { File.OpenRead("split_3.apk"), File.OpenRead("split_4.apk") }, "com.android.app"); // Add split app to base app which packagename is 'com.android.app' ... } ``` @@ -373,9 +376,9 @@ static void Main(string[] args) static async void Main(string[] args) { ... - System.Drawing.Image img = client.GetFrameBuffer(device, CancellationToken.None); // synchronously + Image img = client.GetFrameBuffer(device, CancellationToken.None); // synchronously ... - System.Drawing.Image img = await client.GetFrameBufferAsync(device, CancellationToken.None); // asynchronously + Image img = await client.GetFrameBufferAsync(device, CancellationToken.None); // asynchronously ... } ``` @@ -398,9 +401,9 @@ void DownloadFile() { using (SyncService service = new SyncService(new AdbSocket(client.EndPoint), device)) { - using (Stream stream = File.OpenWrite(@"C:\MyFile.txt")) + using (FileStream stream = File.OpenWrite(@"C:\MyFile.txt")) { - service.Pull("/data/local/tmp/MyFile.txt", stream, null, CancellationToken.None); + service.Pull("/data/local/tmp/MyFile.txt", stream, null); } } } @@ -409,9 +412,9 @@ void UploadFile() { using (SyncService service = new SyncService(new AdbSocket(client.EndPoint), device)) { - using (Stream stream = File.OpenRead(@"C:\MyFile.txt")) + using (FileStream stream = File.OpenRead(@"C:\MyFile.txt")) { - service.Push(stream, "/data/local/tmp/MyFile.txt", 777 ,DateTimeOffset.Now, null ,CancellationToken.None); + service.Push(stream, "/data/local/tmp/MyFile.txt", 777, DateTimeOffset.Now, null); } } } @@ -439,10 +442,10 @@ AdbClient.SetEncoding(Encoding.ASCII); ``` ## Contributors -[![Contributors](https://contrib.rocks/image?repo=yungd1plomat/AdvancedSharpAdbClient)](https://github.com/yungd1plomat/AdvancedSharpAdbClient/graphs/contributors) +[![Contributors](https://contrib.rocks/image?repo=SharpAdb/AdvancedSharpAdbClient)](https://github.com/SharpAdb/AdvancedSharpAdbClient/graphs/contributors) ## Consulting and Support -Please open an [**issue**](https://github.com/yungd1plomat/AdvancedSharpAdbClient/issues) on if you have suggestions or problems. +Please open an [**issue**](https://github.com/SharpAdb/AdvancedSharpAdbClient/issues) on if you have suggestions or problems. ## History AdvancedSharpAdbClient is a fork of [SharpAdbClient](https://github.com/quamotion/madb) and [madb](https://github.com/camalot/madb) which in itself is a .NET port of the [ddmlib Java Library](https://android.googlesource.com/platform/tools/base/+/master/ddmlib/).