Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/Essentials/src/FileSystem/FileSystemUtils.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ public static string EnsurePhysicalPath(AndroidUri uri, bool requireExtendedAcce

// try resolve using the content provider
var absolute = ResolvePhysicalPath(uri, requireExtendedAccess);
if (!string.IsNullOrWhiteSpace(absolute) && Path.IsPathRooted(absolute))
// On API 28 and below, ResolvePhysicalPath may return a raw filesystem path that passes
// File.Exists() but lacks read permission, causing UnauthorizedAccessException.
// IsFileReadable uses Java.IO.File.canRead() to verify actual read access before using the path.
// On API 29+, ResolvePhysicalPath typically returns null for content URIs, so the fallback path is used instead.
if (!string.IsNullOrWhiteSpace(absolute) && Path.IsPathRooted(absolute) && IsFileReadable(absolute))
return absolute;

// fall back to just copying it
Expand All @@ -71,6 +75,12 @@ public static string EnsurePhysicalPath(AndroidUri uri, bool requireExtendedAcce
throw new FileNotFoundException($"Unable to resolve absolute path or retrieve contents of URI '{uri}'.");
}

internal static bool IsFileReadable(string path)
{
using var file = new Java.IO.File(path);
return file.IsFile && file.CanRead();
}

static string ResolvePhysicalPath(AndroidUri uri, bool requireExtendedAccess = true)
{
if (uri.Scheme.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.IO;
using Microsoft.Maui.Storage;
using Xunit;

namespace Microsoft.Maui.Essentials.DeviceTests.Shared
{
[Category("FileSystem")]
public class Android_FileSystemUtils_Tests
{
[Fact]
public void IsFileReadable_Returns_True_For_Readable_File()
{
var filePath = Path.Combine(FileSystem.CacheDirectory, "readable_test.txt");
try
{
File.WriteAllText(filePath, "test content");
Assert.True(FileSystemUtils.IsFileReadable(filePath));
}
finally
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
}

[Fact]
public void IsFileReadable_Returns_False_For_NonExistent_File()
{
var filePath = Path.Combine(FileSystem.CacheDirectory, "nonexistent_file_12345.txt");
Assert.False(FileSystemUtils.IsFileReadable(filePath));
}

[Fact]
public void IsFileReadable_Returns_False_For_Inaccessible_Path()
{
// Create a real file then revoke read permission for a deterministic test
var filePath = Path.Combine(FileSystem.CacheDirectory, "unreadable_test.txt");
try
{
File.WriteAllText(filePath, "test content");
using var javaFile = new Java.IO.File(filePath);
if (!javaFile.SetReadable(false))
{
return; // Permission change not supported on this device/config — skip test
}

Assert.False(FileSystemUtils.IsFileReadable(filePath));
}
finally
{
if (File.Exists(filePath))
{
// Restore readability before cleanup to return the file to a normal state
using var javaFile = new Java.IO.File(filePath);
javaFile.SetReadable(true);
File.Delete(filePath);
}
}
}
}
}
Loading