diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 166b98436e..83776ec2c1 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -144,9 +144,12 @@
-
+
+
+
+
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index 1726028a3f..a9bf150ff9 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -60,7 +60,7 @@ namespace ControlCatalog
{
if (flowDirections.SelectedItem is FlowDirection flowDirection)
{
- this.FlowDirection = flowDirection;
+ TopLevel.GetTopLevel(this).FlowDirection = flowDirection;
}
};
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml
index 5325534909..90c717e7ed 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml
@@ -1,6 +1,8 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:storage="clr-namespace:Avalonia.Platform.Storage;assembly=Avalonia.Base"
+ xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections">
@@ -42,6 +44,19 @@
+
+
+
+ Desktop
+ Documents
+ Downloads
+ Pictures
+ Videos
+ Music
+
+
+
+
("PickerLastResults");
var resultsVisible = this.Get("PickerLastResultsVisible");
var bookmarkContainer = this.Get("BookmarkContainer");
var openedFileContent = this.Get("OpenedFileContent");
var openMultiple = this.Get("OpenMultiple");
+ var currentFolderBox = this.Get("CurrentFolderBox");
+
+ currentFolderBox.TextChanged += async (sender, args) =>
+ {
+ if (ignoreTextChanged) return;
+
+ if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum))
+ {
+ lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum);
+ }
+ else
+ {
+ if (!Uri.TryCreate(currentFolderBox.Text, UriKind.Absolute, out var folderLink))
+ {
+ Uri.TryCreate("file://" + currentFolderBox.Text, UriKind.Absolute, out folderLink);
+ }
+
+ if (folderLink is not null)
+ {
+ lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink);
+ }
+ }
+ };
- IStorageFolder? lastSelectedDirectory = null;
List GetFilters()
{
@@ -84,7 +109,7 @@ namespace ControlCatalog.Pages
{
Title = "Open multiple files",
Filters = GetFilters(),
- Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
+ Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
@@ -97,7 +122,7 @@ namespace ControlCatalog.Pages
{
Title = "Save file",
Filters = filters,
- Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
+ Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
@@ -109,7 +134,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFolderDialog()
{
Title = "Select folder",
- Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null
+ Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
}.ShowAsync(GetWindow());
if (string.IsNullOrEmpty(result))
{
@@ -117,7 +142,7 @@ namespace ControlCatalog.Pages
}
else
{
- lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result));
+ SetFolder(await GetStorageProvider().TryGetFolderFromPath(result));
results.Items = new[] { result };
resultsVisible.IsVisible = true;
}
@@ -127,7 +152,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFileDialog()
{
Title = "Select both",
- Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
+ Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
@@ -210,7 +235,7 @@ namespace ControlCatalog.Pages
#endif
await reader.WriteLineAsync(openedFileContent.Text);
- lastSelectedDirectory = await file.GetParentAsync();
+ SetFolder(await file.GetParentAsync());
}
await SetPickerResult(file is null ? null : new[] { file });
@@ -226,7 +251,7 @@ namespace ControlCatalog.Pages
await SetPickerResult(folders);
- lastSelectedDirectory = folders.FirstOrDefault();
+ SetFolder(folders.FirstOrDefault());
};
this.Get
diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
index dcd7a88a56..a097f1f951 100644
--- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
@@ -9,6 +9,7 @@ namespace ControlCatalog.Pages
public class ScrollViewerPageViewModel : ViewModelBase
{
private bool _allowAutoHide;
+ private bool _enableInertia;
private ScrollBarVisibility _horizontalScrollVisibility;
private ScrollBarVisibility _verticalScrollVisibility;
@@ -25,6 +26,7 @@ namespace ControlCatalog.Pages
HorizontalScrollVisibility = ScrollBarVisibility.Auto;
VerticalScrollVisibility = ScrollBarVisibility.Auto;
AllowAutoHide = true;
+ EnableInertia = true;
}
public bool AllowAutoHide
@@ -33,6 +35,12 @@ namespace ControlCatalog.Pages
set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value);
}
+ public bool EnableInertia
+ {
+ get => _enableInertia;
+ set => this.RaiseAndSetIfChanged(ref _enableInertia, value);
+ }
+
public ScrollBarVisibility HorizontalScrollVisibility
{
get => _horizontalScrollVisibility;
diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs
index c06f5c74ec..247008c503 100644
--- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs
+++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs
@@ -1,6 +1,8 @@
using System;
using Android.App;
using Android.Content;
+using Android.Content.PM;
+using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
@@ -13,6 +15,7 @@ namespace Avalonia.Android
internal static object ViewContent;
public Action ActivityResult { get; set; }
+ public Action RequestPermissionsResult { get; set; }
internal AvaloniaView View;
private GlobalLayoutListener _listener;
@@ -82,6 +85,13 @@ namespace Avalonia.Android
ActivityResult?.Invoke(requestCode, resultCode, data);
}
+ public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
+ {
+ base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
+ }
+
class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{
private AvaloniaView _view;
diff --git a/src/Android/Avalonia.Android/IActivityResultHandler.cs b/src/Android/Avalonia.Android/IActivityResultHandler.cs
index 14094ee185..40a8b5cbcf 100644
--- a/src/Android/Avalonia.Android/IActivityResultHandler.cs
+++ b/src/Android/Avalonia.Android/IActivityResultHandler.cs
@@ -1,11 +1,14 @@
using System;
using Android.App;
using Android.Content;
+using Android.Content.PM;
namespace Avalonia.Android
{
public interface IActivityResultHandler
{
public Action ActivityResult { get; set; }
+
+ public Action RequestPermissionsResult { get; set; }
}
}
diff --git a/src/Android/Avalonia.Android/Platform/PlatformSupport.cs b/src/Android/Avalonia.Android/Platform/PlatformSupport.cs
new file mode 100644
index 0000000000..9877f48664
--- /dev/null
+++ b/src/Android/Avalonia.Android/Platform/PlatformSupport.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Android.App;
+using Android.Content;
+using Android.Content.PM;
+
+namespace Avalonia.Android.Platform;
+
+internal static class PlatformSupport
+{
+ private static int s_lastRequestCode = 20000;
+
+ public static int GetNextRequestCode() => s_lastRequestCode++;
+
+ public static async Task CheckPermission(this Activity activity, string permission)
+ {
+ if (activity is not IActivityResultHandler mainActivity)
+ {
+ throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface.");
+ }
+
+ if (!OperatingSystem.IsAndroidVersionAtLeast(23))
+ {
+ return true;
+ }
+
+ if (activity.CheckSelfPermission(permission) == Permission.Granted)
+ {
+ return true;
+ }
+
+ var currentRequestCode = GetNextRequestCode();
+ var tcs = new TaskCompletionSource();
+ mainActivity.RequestPermissionsResult += RequestPermissionsResult;
+ activity.RequestPermissions(new [] { permission }, currentRequestCode);
+
+ return await tcs.Task;
+
+ void RequestPermissionsResult(int requestCode, string[] arg2, Permission[] arg3)
+ {
+ if (currentRequestCode != requestCode)
+ {
+ return;
+ }
+
+ mainActivity.RequestPermissionsResult -= RequestPermissionsResult;
+
+ _ = tcs.TrySetResult(arg3.All(p => p == Permission.Granted));
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
index 078f70db60..9838bb06c8 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
@@ -2,10 +2,11 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Android;
+using Android.App;
using Android.Content;
using Android.Provider;
using Avalonia.Logging;
@@ -19,41 +20,48 @@ namespace Avalonia.Android.Platform.Storage;
internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
- private Context? _context;
+ private Activity? _activity;
+ private readonly bool _needsExternalFilesPermission;
- protected AndroidStorageItem(Context context, AndroidUri uri)
+ protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission)
{
- _context = context;
+ _activity = activity;
+ _needsExternalFilesPermission = needsExternalFilesPermission;
Uri = uri;
}
internal AndroidUri Uri { get; }
+
+ protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
- protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
-
- public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName)
+ public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
+ public Uri Path => new(Uri.ToString()!);
+
public bool CanBookmark => true;
- public Task SaveBookmarkAsync()
+ public async Task SaveBookmarkAsync()
{
- Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
- return Task.FromResult(Uri.ToString());
- }
+ if (!await EnsureExternalFilesPermission(false))
+ {
+ return null;
+ }
- public Task ReleaseBookmarkAsync()
- {
- Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
- return Task.CompletedTask;
+ Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
+ return Uri.ToString();
}
- public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
+ public async Task ReleaseBookmarkAsync()
{
- uri = new Uri(Uri.ToString()!);
- return true;
- }
+ if (!await EnsureExternalFilesPermission(false))
+ {
+ return;
+ }
+ Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
+ }
+
public abstract Task GetBasicPropertiesAsync();
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
@@ -77,29 +85,44 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return null;
}
- public Task GetParentAsync()
+ public async Task GetParentAsync()
{
+ if (!await EnsureExternalFilesPermission(false))
+ {
+ return null;
+ }
+
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
if (javaFile.ParentFile is {} parentFile
&& AndroidUri.FromFile(parentFile) is {} androidUri)
{
- return Task.FromResult(new AndroidStorageFolder(Context, androidUri));
+ return new AndroidStorageFolder(Activity, androidUri, false);
}
- return Task.FromResult(null);
+ return null;
}
+ protected async Task EnsureExternalFilesPermission(bool write)
+ {
+ if (!_needsExternalFilesPermission)
+ {
+ return true;
+ }
+
+ return await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
+ }
+
public void Dispose()
{
- _context = null;
+ _activity = null;
}
}
-internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
+internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
- public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri)
+ public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission)
{
}
@@ -110,6 +133,11 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
public async Task> GetItemsAsync()
{
+ if (!await EnsureExternalFilesPermission(false))
+ {
+ return Array.Empty();
+ }
+
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
@@ -124,8 +152,8 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
.Where(t => t.uri is not null)
.Select(t => t.file switch
{
- { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!),
- { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!),
+ { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!),
+ { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false),
_ => null
})
.Where(i => i is not null)
@@ -133,9 +161,20 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
}
}
+internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
+{
+ public WellKnownAndroidStorageFolder(Activity activity, string identifier, AndroidUri uri, bool needsExternalFilesPermission)
+ : base(activity, uri, needsExternalFilesPermission)
+ {
+ Name = identifier;
+ }
+
+ public override string Name { get; }
+}
+
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{
- public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri)
+ public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
{
}
@@ -143,10 +182,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public bool CanOpenWrite => true;
- public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false)
+ public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
- public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true)
+ public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Activity, Uri, true)
?? throw new InvalidOperationException("Failed to open content stream"));
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)
@@ -210,7 +249,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded,
MediaStore.IMediaColumns.DateModified
};
- using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null);
+ using var cursor = Activity.ContentResolver!.Query(Uri, projection, null, null, null);
if (cursor?.MoveToFirst() == true)
{
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
index 62e43ff2ef..f611f50164 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
@@ -4,18 +4,21 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Android;
using Android.App;
using Android.Content;
using Android.Provider;
using Avalonia.Platform.Storage;
+using Java.Lang;
using AndroidUri = Android.Net.Uri;
+using Exception = System.Exception;
+using JavaFile = Java.IO.File;
namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider
{
private readonly Activity _activity;
- private int _lastRequestCode = 20000;
public AndroidStorageProvider(Activity activity)
{
@@ -31,7 +34,108 @@ internal class AndroidStorageProvider : IStorageProvider
public Task OpenFolderBookmarkAsync(string bookmark)
{
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
- return Task.FromResult(new AndroidStorageFolder(_activity, uri));
+ return Task.FromResult(new AndroidStorageFolder(_activity, uri, false));
+ }
+
+ public async Task TryGetFileFromPath(Uri filePath)
+ {
+ if (filePath is null)
+ {
+ throw new ArgumentNullException(nameof(filePath));
+ }
+
+ if (filePath is not { IsAbsoluteUri: true, Scheme: "file" or "content" })
+ {
+ throw new ArgumentException("File path is expected to be an absolute link with \"file\" or \"content\" scheme.");
+ }
+
+ var androidUri = AndroidUri.Parse(filePath.ToString());
+ if (androidUri?.Path is not {} androidUriPath)
+ {
+ return null;
+ }
+
+ var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
+ if (!hasPerms)
+ {
+ throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
+ }
+
+ var javaFile = new JavaFile(androidUriPath);
+ if (javaFile.Exists() && javaFile.IsFile)
+ {
+ return null;
+ }
+
+ return new AndroidStorageFile(_activity, androidUri);
+ }
+
+ public async Task TryGetFolderFromPath(Uri folderPath)
+ {
+ if (folderPath is null)
+ {
+ throw new ArgumentNullException(nameof(folderPath));
+ }
+
+ if (folderPath is not { IsAbsoluteUri: true, Scheme: "file" or "content" })
+ {
+ throw new ArgumentException("Folder path is expected to be an absolute link with \"file\" or \"content\" scheme.");
+ }
+
+ var androidUri = AndroidUri.Parse(folderPath.ToString());
+ if (androidUri?.Path is not {} androidUriPath)
+ {
+ return null;
+ }
+
+ var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
+ if (!hasPerms)
+ {
+ throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
+ }
+
+ var javaFile = new JavaFile(androidUriPath);
+ if (javaFile.Exists() && javaFile.IsDirectory)
+ {
+ return null;
+ }
+
+ return new AndroidStorageFolder(_activity, androidUri, false);
+ }
+
+ public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
+ {
+ var dirCode = wellKnownFolder switch
+ {
+ WellKnownFolder.Desktop => null,
+ WellKnownFolder.Documents => global::Android.OS.Environment.DirectoryDocuments,
+ WellKnownFolder.Downloads => global::Android.OS.Environment.DirectoryDownloads,
+ WellKnownFolder.Music => global::Android.OS.Environment.DirectoryMusic,
+ WellKnownFolder.Pictures => global::Android.OS.Environment.DirectoryPictures,
+ WellKnownFolder.Videos => global::Android.OS.Environment.DirectoryMovies,
+ _ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
+ };
+ if (dirCode is null)
+ {
+ return Task.FromResult(null);
+ }
+
+ var dir = _activity.GetExternalFilesDir(dirCode);
+ if (dir is null || !dir.Exists())
+ {
+ return Task.FromResult(null);
+ }
+
+ var uri = AndroidUri.FromFile(dir);
+ if (uri is null)
+ {
+ return Task.FromResult(null);
+ }
+
+ // To make TryGetWellKnownFolder API easier to use, we don't check for the permissions.
+ // It will work with file picker activities, but it will fail on any direct access to the folder, like getting list of children.
+ // We pass "needsExternalFilesPermission" parameter here, so folder itself can check for permissions on any FS access.
+ return Task.FromResult(new WellKnownAndroidStorageFolder(_activity, dirCode, uri, true));
}
public Task OpenFileBookmarkAsync(string bookmark)
@@ -110,19 +214,21 @@ internal class AndroidStorageProvider : IStorageProvider
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
var uris = await StartActivity(pickerIntent, false);
- return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray();
+ return uris.Select(u => new AndroidStorageFolder(_activity, u, false)).ToArray();
}
private async Task> StartActivity(Intent? pickerIntent, bool singleResult)
{
var resultList = new List(1);
var tcs = new TaskCompletionSource();
- var currentRequestCode = _lastRequestCode++;
+ var currentRequestCode = PlatformSupport.GetNextRequestCode();
- if (_activity is IActivityResultHandler mainActivity)
+ if (!(_activity is IActivityResultHandler mainActivity))
{
- mainActivity.ActivityResult += OnActivityResult;
+ throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface.");
}
+
+ mainActivity.ActivityResult += OnActivityResult;
_activity.StartActivityForResult(pickerIntent, currentRequestCode);
var result = await tcs.Task;
@@ -161,11 +267,7 @@ internal class AndroidStorageProvider : IStorageProvider
return;
}
-
- if (_activity is IActivityResultHandler mainActivity)
- {
- mainActivity.ActivityResult -= OnActivityResult;
- }
+ mainActivity.ActivityResult -= OnActivityResult;
_ = tcs.TrySetResult(resultCode == Result.Ok ? data : null);
}
diff --git a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs
index 068c190fa1..801f247754 100644
--- a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs
+++ b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Media.Immutable;
+using Avalonia.Media.Transformation;
#nullable enable
@@ -30,7 +31,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableRadialGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
- oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null,
+ InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center),
@@ -41,7 +42,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableConicGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
- oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null,
+ InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center),
@@ -51,7 +52,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableLinearGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
- oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null,
+ InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint),
@@ -72,6 +73,25 @@ namespace Avalonia.Animation.Animators
return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
}
+ private static ImmutableTransform? InterpolateTransform(double progress,
+ ITransform? oldTransform, ITransform? newTransform)
+ {
+ if (oldTransform is TransformOperations oldTransformOperations
+ && newTransform is TransformOperations newTransformOperations)
+ {
+
+ return new ImmutableTransform(TransformOperations
+ .Interpolate(oldTransformOperations, newTransformOperations, progress).Value);
+ }
+
+ if (oldTransform is not null)
+ {
+ return new ImmutableTransform(oldTransform.Value);
+ }
+
+ return null;
+ }
+
private static IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue)
{
var resultCount = Math.Max(oldValue.Count, newValue.Count);
diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs
index a43194153c..31b6cad8ab 100644
--- a/src/Avalonia.Base/AttachedProperty.cs
+++ b/src/Avalonia.Base/AttachedProperty.cs
@@ -24,11 +24,9 @@ namespace Avalonia
Func? validate = null)
: base(name, ownerType, metadata, inherits, validate)
{
+ IsAttached = true;
}
- ///
- public override bool IsAttached => true;
-
///
/// Attaches the property as a non-attached property on the specified type.
///
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index dc94dfba40..1946d4ba5c 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -132,7 +132,7 @@ namespace Avalonia
switch (property)
{
- case StyledPropertyBase styled:
+ case StyledProperty styled:
ClearValue(styled);
break;
case DirectPropertyBase direct:
@@ -147,7 +147,7 @@ namespace Avalonia
/// Clears a 's local value.
///
/// The property.
- public void ClearValue(StyledPropertyBase property)
+ public void ClearValue(StyledProperty property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
@@ -220,7 +220,7 @@ namespace Avalonia
/// The type of the property.
/// The property.
/// The value.
- public T GetValue(StyledPropertyBase property)
+ public T GetValue(StyledProperty property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
@@ -243,7 +243,7 @@ namespace Avalonia
}
///
- public Optional GetBaseValue(StyledPropertyBase property)
+ public Optional GetBaseValue(StyledProperty property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
@@ -309,7 +309,7 @@ namespace Avalonia
/// An if setting the property can be undone, otherwise null.
///
public IDisposable? SetValue(
- StyledPropertyBase property,
+ StyledProperty property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
@@ -373,7 +373,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding.
///
public IDisposable Bind(
- StyledPropertyBase property,
+ StyledProperty property,
IObservable