UnityWebRequestをUniTaskで待つときにdownloadedBytesを受け取れるようにする。

# 環境
UniTask 2.0.19

# デフォルトではUnityWebRequestをUniTaskで待つときにdownloadedBytesを受け取れない

```UnityAsyncExtensions.cs
public static UniTask<UnityWebRequest> ToUniTask(this UnityWebRequestAsyncOperation asyncOperation, IProgress<float> progress = null, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken))
```

UnityWebRequestをToUniTaskでUniTaskに変換するときにIProgressを使って割合は取ってこれるが、downloadedBytesは取れない。
（Content-Lengthでダウンロードサイズ取得してProgress使って算出する等は可能だと思うが、UnityWebRequestのdownloadedBytesが取れないって意味で)

そこで、引数に`Action<long> reportDownloadedBytes`を追加してみる。

# 新しく拡張メソッドを定義

```UnityWebRequestAsyncOperationExt.cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;

namespace DL
{
    public static partial class UnityWebRequestAsyncOperationExt
    {
        public static UniTask<UnityWebRequest> ToUniTaskWithDownloadedBytes(this UnityWebRequestAsyncOperation asyncOperation, IProgress<float> progress = null, Action<ulong> reportDownloadedBytes = null, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (asyncOperation == null)
            {
                throw new ArgumentNullException(nameof(asyncOperation));
            }
            if (asyncOperation.isDone) return UniTask.FromResult(asyncOperation.webRequest);
            return new UniTask<UnityWebRequest>(UnityWebRequestAsyncOperationConfiguredSource.Create(asyncOperation, timing, progress, reportDownloadedBytes, cancellationToken, out var token), token);
        }

        sealed class UnityWebRequestAsyncOperationConfiguredSource : IUniTaskSource<UnityWebRequest>, IPlayerLoopItem, ITaskPoolNode<UnityWebRequestAsyncOperationConfiguredSource>
        {
            static TaskPool<UnityWebRequestAsyncOperationConfiguredSource> pool;
            public UnityWebRequestAsyncOperationConfiguredSource NextNode { get; set; }

            static UnityWebRequestAsyncOperationConfiguredSource()
            {
                TaskPool.RegisterSizeGetter(typeof(UnityWebRequestAsyncOperationConfiguredSource), () => pool.Size);
            }

            UnityWebRequestAsyncOperation asyncOperation;
            IProgress<float> progress;
            Action<ulong> reportDownloadedBytes;
            CancellationToken cancellationToken;

            UniTaskCompletionSourceCore<UnityWebRequest> core;

            UnityWebRequestAsyncOperationConfiguredSource()
            {

            }

            public static IUniTaskSource<UnityWebRequest> Create(UnityWebRequestAsyncOperation asyncOperation, PlayerLoopTiming timing, IProgress<float> progress, Action<ulong> reportDownloadedBytes, CancellationToken cancellationToken, out short token)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return AutoResetUniTaskCompletionSource<UnityWebRequest>.CreateFromCanceled(cancellationToken, out token);
                }

                if (!pool.TryPop(out var result))
                {
                    result = new UnityWebRequestAsyncOperationConfiguredSource();
                }

                result.asyncOperation = asyncOperation;
                result.progress = progress;
                result.reportDownloadedBytes = reportDownloadedBytes;
                result.cancellationToken = cancellationToken;

                TaskTracker.TrackActiveTask(result, 3);

                PlayerLoopHelper.AddAction(timing, result);

                token = result.core.Version;
                return result;
            }

            public UnityWebRequest GetResult(short token)
            {
                try
                {
                    return core.GetResult(token);
                }
                finally
                {
                    TryReturn();
                }
            }

            void IUniTaskSource.GetResult(short token)
            {
                GetResult(token);
            }

            public UniTaskStatus GetStatus(short token)
            {
                return core.GetStatus(token);
            }

            public UniTaskStatus UnsafeGetStatus()
            {
                return core.UnsafeGetStatus();
            }

            public void OnCompleted(Action<object> continuation, object state, short token)
            {
                core.OnCompleted(continuation, state, token);
            }

            public bool MoveNext()
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    asyncOperation.webRequest.Abort();
                    core.TrySetCanceled(cancellationToken);
                    return false;
                }

                if (progress != null)
                {
                    progress.Report(asyncOperation.progress);
                }

                if (reportDownloadedBytes != null)
                {
                    reportDownloadedBytes.Invoke(asyncOperation.webRequest.downloadedBytes);
                }

                if (asyncOperation.isDone)
                {
                    if (asyncOperation.webRequest.isHttpError || asyncOperation.webRequest.isNetworkError)
                    {
                        core.TrySetException(new UnityWebRequestException(asyncOperation.webRequest));
                    }
                    else
                    {
                        core.TrySetResult(asyncOperation.webRequest);
                    }
                    return false;
                }

                return true;
            }

            bool TryReturn()
            {
                TaskTracker.RemoveTracking(this);
                core.Reset();
                asyncOperation = default;
                progress = default;
                cancellationToken = default;
                return pool.TryPush(this);
            }
        }
    }
}


```

内容はほぼUniTaskのをコピペ。
UnityWebRequestAsyncOperationConfiguredSourceのMoveNextで`progress.Report`を呼んでいたため、
その下にreportDownloadedBytesを追加。

# テスト
```UnityWebRequestTest.cs
using System.Collections;
using UnityEngine.Networking;
using UnityEngine.TestTools;
using Cysharp.Threading.Tasks;
using System;
using NUnit.Framework;
using System.IO;
using DL;

namespace DLTests
{
    public class UnityWebRequestTest
    {
        [UnityTest]
        public IEnumerator DownloadFileTest() => UniTask.ToCoroutine(async () =>
        {
            string testDir = "Test/DL/DownloadFileTest";
            string url = "https://teach310.github.io/AssetBundlesForDownloadTest/SBP/Prefabs001";
            string destPath = testDir + "/Prefabs001";
            float timeout = 3f;

            // リセット
            if (Directory.Exists(testDir))
            {
                Directory.Delete(testDir, true);
            }
            Directory.CreateDirectory(testDir);

            ulong downloadedBytes = 0L;
            Action<ulong> reportDownloadedBytes = x => downloadedBytes = x;
            using (var request = UnityWebRequest.Get(url))
            {
                var downloadHandler = new DownloadHandlerFile(destPath);
                downloadHandler.removeFileOnAbort = true;
                request.downloadHandler = downloadHandler;
                try
                {
                    await request.SendWebRequest()
                        .ToUniTaskWithDownloadedBytes(reportDownloadedBytes: reportDownloadedBytes)
                        .Timeout(TimeSpan.FromSeconds(timeout));
                }
                catch (UnityWebRequestException ex)
                {
                    if (ex.IsHttpError)
                    {
                        Assert.Fail($"HTTPError status {request.responseCode} message {ex.Message}");
                    }
                    else if (ex.IsNetworkError)
                    {
                        Assert.Fail($"NetworkError status {request.responseCode} message {ex.Message}");
                    }
                    Assert.Fail(ex.Message);
                }
                catch (TimeoutException ex)
                {
                    Assert.Fail(ex.Message);
                }
                catch (Exception ex)
                {
                    // urlが間違ってるときとかに呼ばれる。
                    Assert.Fail(ex.Message);
                }
            }
            Assert.AreEqual(true, downloadedBytes > 0L);
            Directory.Delete(testDir, true);
        });
    }
}

```

リクエストがIsHttpErrorまたはIsNetworkErrorのときにUnityWebRequestExceptionをthrowするようになっていたことに気づけてよかった。