unoplatform / uno

Build Mobile, Desktop and WebAssembly apps with C# and XAML. Today. Open source and professionally supported.
https://platform.uno
Apache License 2.0
8.63k stars 697 forks source link

Android GetFileFromApplicationUriAsync doesn't work #8618

Closed pkar70 closed 2 years ago

pkar70 commented 2 years ago

Current behavior

Exception **Java.IO.FileNotFoundException:** is thrown.

Expected behavior

File should be 'openable'.

How to reproduce it (as minimally and precisely as possible)

            Uri oPicUri = new Uri("ms-appx:///Assets/1950.gif");
            Windows.Storage.StorageFile oFile;
            try
            {  
                oFile = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(oPicUri);
            }
            catch
            {
                return false;
            }

Exception: **Java.IO.FileNotFoundException:** 'Assets/__1950.gif'

Part of: https://github.com/pkar70/KrakTram

GIF files are present, e.g. X\KrakTram\KrakTram_Uno\KrakTram_Uno.Shared\Assets\1950.gif On build, they are copied to X\KrakTram\KrakTram_Uno\KrakTram_Uno.Droid\obj\Debug\120\res\drawable-nodpi__1950.gif

Workaround

None known.

Works on UWP/WinUI

Yes

Environment

Uno.UI / Uno.UI.WebAssembly / Uno.UI.Skia

NuGet package version(s)

UnoUI 4.2.6

Affected platforms

Android

IDE

Visual Studio 2022

IDE version

17.0.5

Relevant plugins

No response

Anything else we need to know?

No response

jeromelaban commented 2 years ago

Make sure to validate first with this sample application: https://github.com/unoplatform/Uno.Samples/tree/master/UI/PackageResources

GitHub
Uno.Samples/UI/PackageResources at master · unoplatform/Uno.Samples
A collection of code samples for the Uno Platform. Contribute to unoplatform/Uno.Samples development by creating an account on GitHub.
pkar70 commented 2 years ago

Sample has no files in Assets folder, so it is not related. Android head should work same as UWP head, and UWP head works.

jeromelaban commented 2 years ago

Thanks. This is likely this Visual Studio issue, as this has been working in Uno for a while. Apply the workaround, and make sure to upvote the VS issue so it gets fixed.

Also, when you're creating issues, don't point to your full app repository, make sure to create a small sample to attach to the issue.

ghost commented 2 years ago

That seams to be the same problem with the unit tests for PR https://github.com/unoplatform/uno/pull/8370 The PR itself is ok, correcting the issues #5453 and #7288. The tests passed only on iOS / macOS and the reason why is the GetFileFromApplicationUriAsync not loading Assets. The exception is exactly the same on Android.

pkar70 commented 2 years ago

One more check: files are present in .apk file: X\KrakTram\KrakTram_Uno\KrakTram_Uno.Droid\bin\Debug\pkar.KrakTram.apk\res\drawable-nodpi-v4__1950.gif

jeromelaban commented 2 years ago

@pkar70 did you try with the workaround? Does it change anything?

pkar70 commented 2 years ago

No... a) I have no VS 2022 Preview b) files ARE in apk c) after Android R, accessing resources is changed (see below) d) I would test some things by debugging :)

(After Build.VERSION_CODES#R, Resources must be obtained by Activity or Context created with Context.createWindowContext(int, Bundle). [Application#getResources()](https://developer.android.com/reference/android/content/ContextWrapper#getResources()) may report wrong values in multi-window or on secondary displays.

jeromelaban commented 2 years ago

Thanks. Files are in the APK but with an invalid path / file name, which points to a similar shared project related issue.

pkar70 commented 2 years ago

After trying workaround - same. Java.IO.FileNotFoundException. And files are still in same folder within apk: X\KrakTram\KrakTram_Uno\KrakTram_Uno.Droid\bin\Debug\pkar.KrakTram.apk\res\drawable-nodpi-v4\__1950.gif

jeromelaban commented 2 years ago

This reminds me that images are not handled the same way as other file types. If you want images to be read by GetFileFromApplicationUriAsync, you need to include them as AndroidAsset not AndroidResource. This is not something we'll be able to change, as android mangles the file names when included with nesting.

pkar70 commented 2 years ago

I have workaround...

1) copy files from Uno\Shared\Assets\ folder to Uno\Droid\Assets folder This step is required, as files in Uno\Shared\Assets\ are placed (by build process) in res folder, without directory tree.

2) use this as Android replacement for GetFileFromApplicationUri():

        private async System.Threading.Tasks.Task<Windows.Storage.StorageFile> AndroidGetFileFromApplicationUri(Uri uri)
        {
            // "ms-appx:///Assets/" + iRok + ".gif"
            // "__" + iRok + ".gif" w pkar.KrakTram.apk\res\drawable-nodpi-v4\

            if (uri.Scheme != "ms-appx")
            { // we don't handle "ms-appdata://" prefix (app root folder inside user) - limit of this implementation
                throw new InvalidOperationException("Uri is not using the ms-appx scheme");
            }

            string sFilename = uri.ToString();

            if (!sFilename.StartsWith("ms-appx:///Assets/"))
            { // limit of this implementation
                throw new InvalidOperationException("Uri is not ms-appx:///Assets/");
            }

            sFilename = sFilename.Substring("ms-appx:///Assets/".Length);

            var assetMan = Android.App.Application.Context.Assets;
            if (assetMan is null)
            {
                throw new InvalidOperationException("Cannot get AssetManager");
            }

            var outputCachePath = System.IO.Path.Combine(Android.App.Application.Context.CacheDir.AbsolutePath, sFilename);

            if (!System.IO.File.Exists(outputCachePath))
            {
                System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outputCachePath));

                using System.IO.Stream fileInApk = assetMan.Open(sFilename);
                using var output = System.IO.File.OpenWrite(outputCachePath);

                await fileInApk.CopyToAsync(output);
            }

            return await Windows.Storage.StorageFile.GetFileFromPathAsync(outputCachePath);
        }
pkar70 commented 2 years ago

I don't know WHAT places files from Shared\Assets to Droid\res, and how to prevent it.

jeromelaban commented 2 years ago

There's currently no way to change this:

https://github.com/unoplatform/uno/blob/da4338ddc48e0204f0badcb32bdd9ba375a91938/src/SourceGenerators/Uno.UI.Tasks/Assets/RetargetAssets.cs#L131-L137

The only workaround to handle this is to add the items as AndroidAssets manually, like this:

<ItemGroup>
   <AndroidAsset Include="../MyProject.Shared/Assets/**" />
</ItemGroup>
GitHub
uno/RetargetAssets.cs at master · unoplatform/uno
Build Mobile, Desktop and WebAssembly apps with C# and XAML. Today. Open source and professionally supported. - uno/RetargetAssets.cs at master · unoplatform/uno
pkar70 commented 2 years ago

So, if I add this AndroidAsset to csproj in Uno.Droid, files from Uno.Shared\Assets would be visible inside apk under assets folder?

jeromelaban commented 2 years ago

It should be yes, let us know if that helps.

ghost commented 2 years ago

Hummmm Following the outputCachePath var, I did a shot in the dark, changing the value of PATH in:

https://github.com/unoplatform/uno/blob/da4338ddc48e0204f0badcb32bdd9ba375a91938/src/Uno.UWP/Storage/StorageFile.android.cs#L54

from 'Assets/myfile.jpeg' to only 'myfile.jpeg' and "voilà"! Android could load perfectly the image.

I don't know why path has the 'Assets/' since we are using outputCachePath as well.

@jeromelaban

jeromelaban commented 2 years ago

@iury-kc yes, that's what it's supposed to be doing somehow, but the problem is the handling of conflicts in file names. We've been trying to adjust this for a while (https://github.com/unoplatform/uno/pull/7328) but it's quite tricky.

pkar70 commented 2 years ago

But what if I have such structure: Assets +- Digital | +- 10.png | +- 20.png +- Analog | +- 10.png | +- 20.png

similar to https://github.com/pkar70/ZegarSloneczny/tree/master/ZegarSloneczny/pic

'Flattening' dirtree is bad.

GitHub
ZegarSloneczny/ZegarSloneczny/pic at master · pkar70/ZegarSloneczny
Contribute to pkar70/ZegarSloneczny development by creating an account on GitHub.
jeromelaban commented 2 years ago

Flattening is what android does with resources, it's not something we can change. Assets don't have that problem, when using AndroidAsset solution I provided above.

pkar70 commented 2 years ago

Still.. I have UWP app with pictures inside Assets, and GetFileFromApplicationUriAsync() doesn't work for this (on Android).

            Uri oPicUri = new Uri("ms-appx:///Assets/" + iRok + ".gif");
            Windows.Storage.StorageFile oFile;
            oFile = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(oPicUri);
            Windows.UI.Xaml.Media.Imaging.BitmapImage oBmp = new Windows.UI.Xaml.Media.Imaging.BitmapImage();
            oBmp.UriSource = oPicUri;

And I cannot use such code for Android. Because build moves files from Uno.Shared\Assets to \res, and GetFileFromApplicationUriAsync throws exception that file is not found.

I can replace this code with "special code for Android", but code should work correctly (or show Uno0001 warning that it is unimplemented). Maybe, in this case, there should be e.g. "Uno0002 Limited implementation for Android, works only for non-picture files, and only for ms-appx (not ms-appdata)" ? And same in all other places where Uno's implementation is somehow limited (e.g., because target platform has limits).

jeromelaban commented 2 years ago

Can you add a simple repro for your scenario here please (not one from your github) ? We'll make modifications to it and see what's needed.

pkar70 commented 2 years ago

Simple repro: 1) add picture to Uno.Shared/Assets folder, e.g. 1950.gif 2) add code to e.g. MainPage.Loaded

            Uri oPicUri = new Uri("ms-appx:///Assets/1950.gif");
            var oFile = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(oPicUri);
jeromelaban commented 2 years ago

This is what I mean by adding a simple repro: Test8618.zip.

The workaround for this is to add the following to the android project:

  <ItemGroup>
    <AndroidAsset Include="..\Test8618.Shared\Assets\**" Link="Assets\Assets\%(RecursiveDir)\%(FileName)%(Extension)" />
  </ItemGroup>

Take a look at the repro to see it in action.

pkar70 commented 2 years ago

Thanks. In next app version, I will use this.