External file access on WSA IL2CPP build

When the app is registered to handle a file extension and a file with that extension is opened the WSA build will be notified of the file in the following auto generated code found in App.xaml.cpp:

void App::OnFileActivated(FileActivatedEventArgs^ args)
{
    String^ appArgs = "File=";

    m_SplashScreen = args->SplashScreen;

    bool firstFileAdded = false;

    for (auto file : args->Files)
    {
        if (firstFileAdded)
        {
            appArgs += ";";
        }
        else
        {
            firstFileAdded = true;
        }

        appArgs += file->Path;
    }

    InitializeUnity(appArgs);
}

I can then get access to this file path from within Unity using the following code:

public class HoloLensExternalDataInput : MonoBehaviour
{
    // Use this for initialization
    void Start ()
    {
        UnityEngine.WSA.Application.windowActivated += Application_windowActivated;
    }

    private void Application_windowActivated(UnityEngine.WSA.WindowActivationState state)
    {
        if (state != UnityEngine.WSA.WindowActivationState.Deactivated)
        {
            var arguments = UnityEngine.WSA.Application.arguments;
            Debug.Log("Application_windowActivated - arguments: " + arguments);

            const string FILE_ARG = "File=";
            const string UIR_ARG = "Uri=";

            if (arguments.StartsWith(FILE_ARG))
            {
                var filesStr = arguments.Substring(FILE_ARG.Length);
                var files = filesStr.Split(';');

                if (files.Length > 0)
                {
                    Debug.Log("External File: " + files[0]);
#if ENABLE_WINMD_SUPPORT
                    var storeageFile = System.Threading.Tasks.Task.Run(async () => { return await Windows.Storage.StorageFile.GetFileFromPathAsync(files[0]); }).Result;
                    Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add(storeageFile);
#endif

                    DataLoadManager.LoadExternalFile(files[0]);
                }
            }
            else if (arguments.StartsWith(UIR_ARG))
            {
                var uri = arguments.Substring(UIR_ARG.Length);
                //TODO: handel URI
            }
        }
    }
}

The problem is 2 fold.

First, I shouldn’t have to call “Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add” myself. The auto generated code should do this, otherwise the file path passed into Unity is useless and cannot be accessed.

The second problem is that now that I have to compile with IL2CPP instead of the .NET backend because I switched to Unity 2019, this workaround no longer works. One of the two lines after “#if ENABLE_WINMD_SUPPORT” (my guess is the task runner) crashes with the following exception:

UnauthorizedAccessException: Attempted to perform an unauthorized operation.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <00000000000000000000000000000000>:0
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00000] in <00000000000000000000000000000000>:0
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00000] in <00000000000000000000000000000000>:0
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00000] in <00000000000000000000000000000000>:0
at System.Runtime.CompilerServices.StrongBox1[T]..ctor (T value) [0x00000] in <00000000000000000000000000000000>:0 at EWIHARParser.PropertyValue+<GetEnumerator>d__8..ctor (System.Int32 <>1__state) [0x00000] in <00000000000000000000000000000000>:0 at System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner.InvokeMoveNext (System.Object stateMachine) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.ContextCallback.Invoke (System.Object state) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0 at System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner.Run () [0x00000] in <00000000000000000000000000000000>:0 at System.Action.Invoke () [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction (System.Action action, System.Boolean allowInlining, System.Threading.Tasks.Task& currentTask) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.Task.FinishContinuations () [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.Task.FinishStageThree () [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.Task.FinishStageTwo () [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.Task.Finish (System.Boolean bUserDelegateExecuted) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.Task1[TResult].TrySetException (System.Object exceptionObject) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.TaskCompletionSource1[TResult].TrySetException (System.Exception exception) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.AsyncInfoToTaskBridge1[TResult].Complete (Windows.Foundation.IAsyncInfo asyncInfo, System.Func2[T,TResult] getResultsFunction, Windows.Foundation.AsyncStatus asyncStatus) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.AsyncInfoToTaskBridge1[TResult].CompleteFromAsyncOperation (Windows.Foundation.IAsyncOperation1[TResult] asyncInfo, Windows.Foundation.AsyncStatus asyncStatus) [0x00000] in <00000000000000000000000000000000>:0 at Windows.Foundation.AsyncOperationCompletedHandler1[TResult].Invoke (Windows.Foundation.IAsyncOperation1[TResult] asyncInfo, Windows.Foundation.AsyncStatus asyncStatus) [0x00000] in <00000000000000000000000000000000>:0 Rethrow as AggregateException: One or more errors occurred. at System.Threading.Tasks.Task.ThrowIfExceptional (System.Boolean includeTaskCanceledExceptions) [0x00000] in <00000000000000000000000000000000>:0 at System.Threading.Tasks.Task1[TResult].GetResultCore (System.Boolean waitCompletionNotification) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task`1[TResult].get_Result () [0x00000] in <00000000000000000000000000000000>:0
at HoloLensExternalDataInput.Application_windowActivated (UnityEngine.WSA.WindowActivationState state) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.WSA.WindowActivated.Invoke (UnityEngine.WSA.WindowActivationState state) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.WSA.Application.InvokeWindowActivatedEvent (UnityEngine.WSA.WindowActivationState state) [0x00000] in <00000000000000000000000000000000>:0

My preferred fix would be for Unity to add the call to “Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add” to the auto generated code in App.xaml.cpp. But until that is done can someone help me figure out a workaround?

Isn’t FutureAccessList only needed if you want to access that file in subsequent app launches?

Note that if you modify App.xaml.cpp and add FutureAccessList code there, Unity will not overwrite it when you build your project on top of it. It was designed this way so you could put those files into your source control and make any changes you want.

As for the exception - I suppose it didn’t use to happen on .NET scripting backend?

That’s correct. The exception didn’t happen with the .NET scripting backend. As for using the file without adding it to the FutureAccessList, this used to work on a previous version of the HoloLens OS, but once I updated it to the latest a few months ago it stopped working. I could no longer access that file path passed into Unity with regular .NET file IO classes, without first adding it to the FutureAccessList.

I will try modifying App.xaml.cpp directly to work around this issue.

Do you mind filling a bug report for this? If it worked on .NET but doesn’t work on IL2CPP, we consider it a pretty high priority issue.

Ok I’ll file a bug report. It appears calling any async task based code causes it to crash.

I also tried adding the call to FutureAccessList directly to App.xaml.cpp, and that compiles and runs without any errors, but it no longer fixes the problem. Even after adding the file path to the FutureAccessList, it remains inaccessible. Calling File.Copy on it produces a FileNotFound exception. Calling File.SetAttributes produces a security exception. Passing the path to a native code library that tries to read it also fails. I have not been able to find a workaround for this.

How does the native code try to read it? What is the actual path to the file? Does it work if you try to access the file using StorageFile instead of System.IO APIs from C#?

[XamlViewManager] SwitchToView OnCompleted callback status : 1
Application_windowActivated - arguments: File=C:\Data\Users\steve\Downloads\Nanobox Assembly.plmx

Exception thrown at 0x7688F322 in JTViewer.exe: Microsoft C++ exception: Il2CppExceptionWrapper at memory location 0x07BEECD4.
FileNotFoundException: C:\Data\Users\steve\Downloads\Nanobox Assembly.plmx does not exist
at System.IO.File.Copy (System.String sourceFileName, System.String destFileName, System.Boolean overwrite) [0x00000] in <00000000000000000000000000000000>:0
at System.IO.File.Copy (System.String sourceFileName, System.String destFileName) [0x00000] in <00000000000000000000000000000000>:0
at DataLoadManager.Update () [0x00000] in <00000000000000000000000000000000>:0

I believe the native code just uses a C++ file stream.

Both the System.IO and native code file access used to work with the .NET backend after the file path was added to the FutureAccessList. (It also used to work without being added to FutureAccessList on a previous older version of the HoloLens OS)

I have not been able to get code that uses StorageFile to work because it produces an exception while trying to run any async Threading.Tasks code using IL2CPP.

Instead of using Task.Run(() => …).Result, you should be able to just await the storage operations directly without Task.Run. Does that not work?

I’ll be looking at the IL2CPP file operations. I don’t have the solution right now, but I hope to have one soon.

I believe that using the await keyword just uses the same taskrunner code under the hood, but don’t quote me on that. I did try some C# code using await file operations and it produced the same kind of task running exception above when ran on IL2CPP.

That doesn’t sound right, but I can dig in. Can you by any chance make a repro project that showcases this issue? I tried using StorageFile on my end and things seem to work. It could be you’re hitting some edge case with the file picker or something.

I’ll try making a sample app that reproduces the issue. Some differences that you may not be reproducing might be.

  1. I’m running on a HoloLens
  2. I’m not using a file picker. I associate the app with a certain file extension and the OS opens my app when I try to open a file with that extension.
  3. I need to access the file slightly later than within the event that notifies me the file has been picked. So I store the file path and access it on the next Update cycle.

Have you at least been able to confirm that the file can’t be accessed using regular .NET IO classes? And shouldn’t it be accessible from regular .NET IO classes?

Yes, I was able to confirm that. It looks like .NET wrapped StorageFile APIs in order to allow access to files under the hood if regular Windows API access failed. IL2CPP doesn’t do that, so it fails with access denied exception. That is something I want to fix to match .NET behaviour.

Ok I think I’ve finally found a workaround.

You have to add “Windows::Storage::AccessCache::StorageApplicationPermissions::FutureAccessList->Add(file);” into the for loop in the OnFileActivated method of App.xaml.cpp.

Then you can use the following code later to copy the file to the app specific temp folder. After which the copy can be accessed with regular IO classes.

public class HoloLensExternalDataInput : MonoBehaviour
{
    // Use this for initialization
    void Start ()
    {
        UnityEngine.WSA.Application.windowActivated += Application_windowActivated;
    }

    private void Application_windowActivated(UnityEngine.WSA.WindowActivationState state)
    {
        if (state != UnityEngine.WSA.WindowActivationState.Deactivated)
        {
            var arguments = UnityEngine.WSA.Application.arguments;
            Debug.Log("Application_windowActivated - arguments: " + arguments);

            const string FILE_ARG = "File=";
            const string UIR_ARG = "Uri=";

            if (arguments.StartsWith(FILE_ARG))
            {
                var filesStr = arguments.Substring(FILE_ARG.Length);
                var files = filesStr.Split(';');

                if (files.Length > 0)
                {
#if ENABLE_WINMD_SUPPORT
                    LoadExternalFile(files[0]);
#else
                    DataLoadManager.LoadExternalFile(files[0]);
#endif
                }
            }
            else if (arguments.StartsWith(UIR_ARG))
            {
                var uri = arguments.Substring(UIR_ARG.Length);
                //TODO: handel URI
            }
        }
    }

#if ENABLE_WINMD_SUPPORT
    private async void LoadExternalFile(string filePath)
    {
        Windows.Storage.StorageFile storageFile = await Windows.Storage.StorageFile.GetFileFromPathAsync(filePath);

        Windows.Storage.StorageFile copiedFile = await storageFile.CopyAsync(Windows.Storage.ApplicationData.Current.TemporaryFolder, storageFile.Name, Windows.Storage.NameCollisionOption.ReplaceExisting);

        DataLoadManager.LoadExternalFile(copiedFile.Path);
    }
#endif
}

Hello! I’m looking for the same solution. It looks that there is no App.xaml.cpp anymore, and also UnityEngine.WSA.Application.arguments is empty if I pick a file as “Open With”. Do you know how to make it work with the lates Unity versions maybe? Thanks anyway!

App.xaml.cpp is there when you build with XAML build type. The default is D3D build type, which is a bit more efficient. The equivalent there is just App.cpp.

Hello! What if I don’t know C++? Does this mean I can’t implement this feature and Unity can’t handle “Open With…” on UWP out of the box?

Hard to say… This feature is not something we support in Unity, so you’ll have to implement it by using platform APIs directly.

It shouldn’t be too hard to implement, but I don’t think you can avoid C++ code entirely. I doubt you need to learn entirely of C++ to implement it.

Ok, thank you!

Found a solution here: How to access Application.OnFileActivated if the app was launched as "Open With" for an image?