Issue with Addressables and Play Asset Delivery (PAD)

I’m working on a Unity project (6000.0.23f1) using Addressables (2.2.2) and Addressables for Android (1.0.4), configured to use Play Asset Delivery (PAD), and tested via Google Play’s internal test track. Some things seem to function as expected:

  • All asset pack data appear in the game.
  • Google Play Console reflects the Addressable groups with the correct delivery type (e.g., “on demand,” “install time”).
  • Play Store installs the app and some additional data separately (likely the install time asset packs).

But, my on-demand asset packs behave as though they’re included in the base module or cached:

  1. No delay on first access : After a fresh install, loading on-demand assets happens seemingly instantly.
  2. Addressables.GetDownloadSizeAsync(key) always returns 0 before accessing the pack data, suggesting the assets are already present.

I realize that I could just avoid on-demand and use fast-follow or install-time on everything, but the on-demand asset packs are optional DLC packs, so it would be ideal to only download them on purchase.

I believe that PAD on-demand Addressable groups should use Local Build & Load Paths (even though the content is hosted on Google Play). Per the docs, Unity overrides these paths to Local anyways:
https://docs.unity3d.com/Packages/com.unity.addressables.android@1.0/manual/build-for-pad.html

I suspected that GetDownloadSizeAsync might be returning 0 due to caching, but after clearing app cache, data, and reinstalling, the behavior persists. This makes me think the asset packs might be either included in the base app module or cached outside my app’s folder on Android.

My questions are:

  1. Can I rely on the Addressables API (e.g., GetDownloadSizeAsync) to determine whether packs are downloaded, or do I need to use the AndroidAssetPacks API?
  2. Is there a better way to confirm that asset packs are being downloaded on-demand as intended?
  3. Did anyone successfully setup Addressables and on demand PAD?
  4. Do you see anything wrong with my configurations?

Below are some of my settings. Let me know if you need more info.

Any insights or suggestions on troubleshooting this would be much appreciated, since I have already spent two full days getting nowhere on this problem!

1 Like

Just did a new test with AndroidAssetPacks.GetAssetPackStateAsync and all my on-demand packs return a status of “Completed” on a fresh install before triggering any downloads.

I tried clearing the AssetBundle cache and removing all AndroidAssetPacks manually (in the Android build downloaded from Google Play internal test track) by calling the following code and got no exceptions.

However, it still does not work and AndroidAssetPacks.GetAssetPackStateAsync still returns “Completed” on all packs with no errors.

try
{
    foreach (string packName in packNames)
    {
        AndroidAssetPacks.RemoveAssetPack(packName);

        Debug.Log
        (
            Caching.ClearAllCachedVersions(packName)
                ? $"Cleared all versions of AssetBundle '{packName}'."
                : $"Did not clear all versions of AssetBundle '{packName}'."
        );
    }

    Caching.ClearCache();
    Debug.Log("Cleared entire AssetBundle cache.");
}
catch (Exception e)
{
    Debug.LogError($"Clear AssetBundle Cache Exception: {e}");
}

Any Unity staff that can help me out? :slight_smile:

After some more digging, I found that if I check the AndroidAssetPacks.GetAssetPackStateAsync right after removing the asset packs and clearing the cache (before restarting the app), all packs correctly display a status of “not installed”, but immediately on app restart (before downloading anything) all packs are back to “completed” status, which could suggest that there is some kind of hidden caching mecanism going on outside of my control. Maybe something Google Play does to reduce asset pack downloads?

Can anyone confirm if this is a thing?

I’m facing exactly the same issue as described. I’m using Unity 6000.0.32f1, Addressables (2.2.2), and Addressables for Android (1.0.4).

No matter if the asset packs are configured as OnDemand or FastFollow, Addressables.GetDownloadSizeAsync() always returns 0. This behavior misleads the system into thinking the asset packs are available locally, even when they aren’t fully downloaded or accessible.

For example, I’ve implemented a custom system to check the size of asset packs and prompt the user for download when necessary. However, because GetDownloadSizeAsync always returns 0, the system assumes the packs are already available. The actual issue only becomes apparent during GetAssetAsync, where an exception is thrown because the Addressables system tries to load an asset from a pack that hasn’t been downloaded.

This behavior breaks any attempts to handle such situations gracefully. The app can’t warn the user or provide appropriate feedback, as all preloading checks suggest the pack is ready, even when it’s not.

It seems that downloading OnDemand packs only begins when calling Addressables.LoadAssetAsync, but the Addressables API doesn’t provide a way to verify or handle this beforehand. I’ve also tried using Addressables.DownloadDependenciesAsync, but it doesn’t resolve the issue either.

This behavior severely impacts the ability to use Play Asset Delivery as intended, especially for OnDemand packs.

@vladislav_kostyukov

I fixed my issue, though it might not apply to you. My setup had a design flaw.

I used a ScriptableObject with SerializedField references to sprites for a splash screen (before Addressables initialized). Some of these sprites are also part of the asset packs (Addressable groups). The sprites were accessed directly via the ScriptableObject for the splash screen and as AssetReferenceSprite elsewhere in the code base. The ScriptableObject was Addressable and loaded through the Addressables API.

The goal was to include the SerializedField referenced sprites in the base build and the rest in asset packs, but instead it seems to preload or bundle the asset pack, that the sprite belongs to, into the base module, even though the loaded ScriptableObject only referenced the “regular” sprites, not AssetReferenceSprite.

I couldn’t find documentation on this specific issue, but mixing serialized references with Addressables might just be bad practice.

When I made the ScriptableObject non-Addressable and stopped loading it as Addressable, the preloading/bundling stopped.

So my suggestion would be to double-check if any of your asset pack Addressable assets are loaded in any way before you check the download size. Addressables.LoadAssetAsync for a single asset will also download the entire asset pack it belongs to.

I also ditched Addressables.GetDownloadSizeAsync and now fully use the AndroidAssetPacks API for checking and downloading asset packs, as it seems more robust.

Here’s my code using the AndroidAssetPacks API. It depends on UniTask but you should be able to port it to Coroutines if you don’t use UniTask. Hope it helps!

PackAvailabilityService.cs:

using Cysharp.Threading.Tasks;
using Runtime.Models;
using UnityEngine;
using UnityEngine.Android;

namespace Runtime.Services.PackAvailability
{
    public class PackAvailabilityService : IPackAvailabilityService
    {
        async UniTask<bool> IPackAvailabilityService.IsPackDownloaded(string key)
        {
            GetAssetPackStateAsyncOperation stateAsyncOperation = AndroidAssetPacks.GetAssetPackStateAsync(new[] { key });
            await stateAsyncOperation;

            Debug.Log($"PackAvailabilityService.IsPackDownloaded '{key}' // assetPackPath: {AndroidAssetPacks.GetAssetPackPath(key)}");
            Debug.Log($"PackAvailabilityService.IsPackDownloaded '{key}' // stateAsyncOperation // size: {stateAsyncOperation.size} | isDone: {stateAsyncOperation.isDone} | states.Length: {stateAsyncOperation.states.Length}");

            if (stateAsyncOperation.states.Length == 0)
                return false;

            for (int i = 0; i < stateAsyncOperation.states.Length; i++)
            {
                AndroidAssetPackState state = stateAsyncOperation.states[i];
                Debug.Log($"PackAvailabilityService.IsPackDownloaded '{key}' // state ({i}) // name: {state.name} | error: {state.error} | status: {state.status}");
            }

            return stateAsyncOperation.states[0].status == AndroidAssetPackStatus.Completed;
        }
    }
}

DownloadService.cs:

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Runtime.Models;
using Runtime.Services.PackAvailability;
using UnityEngine;
using UnityEngine.Android;
using Zenject;

namespace Runtime.Services.Download
{
    public class DownloadService : IDownloadService
    {
        [Inject]
        private IPackAvailabilityService packAvailabilityService;

        private bool isDownloading;
        private CancellationTokenSource cancellationTokenSource;

        public event Action OnDownloadSuccess;
        public event Action<DownloadEventArgs> OnDownloadRequested;
        public event Action<float> OnDownloadPercentChanged;

        public async UniTaskVoid RequestDownloadAsync(WordPack pack)
        {
            Debug.Log($"DownloadService.RequestDownloadAsync: {pack.Key}");

            // long size = await AddressableUtils.GetDownloadSizeAsync(pack.Key);
            GetAssetPackStateAsyncOperation stateAsyncOperation = AndroidAssetPacks.GetAssetPackStateAsync(new[] { pack.Key });
            await stateAsyncOperation;

            OnDownloadRequested?.Invoke
            (
                new DownloadEventArgs
                (
                    wordPack: pack,
                    downloadSizeMb: stateAsyncOperation.size / 1000000f,
                    networkReachability: Application.internetReachability
                )
            );
        }

        public async UniTask DownloadPackAsync(WordPack pack)
        {
            Debug.Log($"DownloadService.DownloadPackAsync: {pack.Key}");

            if (isDownloading)
                throw new InvalidOperationException("A download is already in progress.");

            isDownloading = true;
            cancellationTokenSource = new CancellationTokenSource();

            try
            {
                string[] packNames = { pack.Key };
                DownloadAssetPackAsyncOperation downloadOp = AndroidAssetPacks.DownloadAssetPackAsync(packNames);

                while (!downloadOp.isDone)
                {
                    if (cancellationTokenSource.Token.IsCancellationRequested)
                    {
                        AndroidAssetPacks.CancelAssetPackDownload(packNames);
                        throw new OperationCanceledException();
                    }

                    OnDownloadPercentChanged?.Invoke(downloadOp.progress);
                    await UniTask.Yield();
                }

                await UniTask.Yield();

                GetAssetPackStateAsyncOperation stateAsyncOperation = AndroidAssetPacks.GetAssetPackStateAsync(packNames);
                await stateAsyncOperation;
                AndroidAssetPackState state = stateAsyncOperation.states[0];

                if (state.status == AndroidAssetPackStatus.Completed)
                {
                    OnDownloadPercentChanged?.Invoke(1f);
                    await packAvailabilityService.UpdatePackStatusAsync(pack);
                    OnDownloadSuccess?.Invoke();
                }
                else
                {
                    // TODO: Add user facing error message
                    throw new Exception($"Something went wrong downloading pack {pack.Key}. Current pack status: {state.status}");
                }
            }
            catch (Exception e) when (e is not OperationCanceledException)
            {
                Debug.LogError(e);
            }
            finally
            {
                isDownloading = false;
                cancellationTokenSource.Dispose();
                cancellationTokenSource = null;
            }
        }

        public void CancelDownload()
        {
            if (!isDownloading || cancellationTokenSource == null)
                return;

            cancellationTokenSource.Cancel();
            isDownloading = false;
        }

        public void ClearPacksCache(string[] onDemandPacks)
        {            
            try
            {
                foreach (string packName in onDemandPacks)
                {
                    AndroidAssetPacks.RemoveAssetPack(packName);

                    Debug.Log
                    (
                        Caching.ClearAllCachedVersions(packName)
                            ? $"Cleared all versions of AssetBundle '{packName}'."
                            : $"Did not clear all versions of AssetBundle '{packName}'."
                    );
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"Clear AssetBundle Cache Exception: {e}");
            }
        }
    }
}