-
Notifications
You must be signed in to change notification settings - Fork 29
feat: MediaGallery
#1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MartinZikmund
wants to merge
15
commits into
main
Choose a base branch
from
dev/mazi/mediagallery
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: MediaGallery
#1146
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
42242b1
chore: Start adding MediaGallery helper
MartinZikmund 889b460
test: Add MediaGalleryHelper sample
MartinZikmund 23d966f
chore: Avoid compilation issue in NavigationBarTests
MartinZikmund 648e32d
chore: Update sample with business logic
MartinZikmund 3c36c6f
feat: Save media to gallery on Android
MartinZikmund d2824e0
chore: iOS permissions adjustment
MartinZikmund a1f2d07
feat: Save media to gallery on iOS
MartinZikmund 1f0282a
chore: Remove overwrite feature
MartinZikmund cb63db8
chore: Add required permission on iOS
MartinZikmund a66c75d
chore: Add XamlRoot
MartinZikmund 444cda4
chore: Apply suggestions from code review
MartinZikmund 8db1f4c
chore: Adjustments based on code-review
MartinZikmund 926abd4
docs: MediaGallery
MartinZikmund 606ffd3
chore: Add permissions information
MartinZikmund 0a3a0e4
chore: Add info on targetFileName
MartinZikmund 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
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
...lkit.Samples/Uno.Toolkit.Samples.Shared/Content/Helpers/MediaGalleryHelperSamplePage.xaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<Page | ||
x:Class="Uno.Toolkit.Samples.Content.Helpers.MediaGalleryHelperSamplePage" | ||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | ||
xmlns:helpers="using:Uno.Toolkit.Samples.Content.Helpers" | ||
xmlns:local="using:Uno.Toolkit.Samples.Content.Helpers" | ||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | ||
xmlns:sample="using:Uno.Toolkit.Samples" | ||
xmlns:utu="using:Uno.Toolkit.UI" | ||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" | ||
mc:Ignorable="d"> | ||
|
||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> | ||
<sample:SamplePageLayout IsDesignAgnostic="True"> | ||
<sample:SamplePageLayout.DesignAgnosticTemplate> | ||
<DataTemplate> | ||
<StackPanel> | ||
<Button Command="{Binding Data.CheckAccessCommand}">Check access</Button> | ||
<Button Command="{Binding Data.SaveCommand}">Save UnoLogo.png to gallery</Button> | ||
<Button Command="{Binding Data.SaveRandomNameCommand}">Save with random name to gallery</Button> | ||
</StackPanel> | ||
</DataTemplate> | ||
</sample:SamplePageLayout.DesignAgnosticTemplate> | ||
</sample:SamplePageLayout> | ||
</Grid> | ||
</Page> |
116 changes: 116 additions & 0 deletions
116
...t.Samples/Uno.Toolkit.Samples.Shared/Content/Helpers/MediaGalleryHelperSamplePage.xaml.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.InteropServices.WindowsRuntime; | ||
using System.Text; | ||
using Uno.Toolkit.Samples.Entities; | ||
using Uno.Toolkit.Samples.Helpers; | ||
using Uno.Toolkit.Samples.ViewModels; | ||
using Uno.Toolkit.UI; | ||
using Windows.Foundation; | ||
using Windows.Foundation.Collections; | ||
using System.Windows.Input; | ||
using System.Net.WebSockets; | ||
using Windows.Storage; | ||
|
||
|
||
|
||
#if IS_WINUI | ||
using Microsoft.UI; | ||
using Microsoft.UI.Xaml; | ||
using Microsoft.UI.Xaml.Controls; | ||
using Microsoft.UI.Xaml.Controls.Primitives; | ||
using Microsoft.UI.Xaml.Data; | ||
using Microsoft.UI.Xaml.Input; | ||
using Microsoft.UI.Xaml.Media; | ||
using Microsoft.UI.Xaml.Navigation; | ||
#else | ||
using Windows.UI; | ||
using Windows.UI.Xaml; | ||
using Windows.UI.Xaml.Controls; | ||
using Windows.UI.Xaml.Controls.Primitives; | ||
using Windows.UI.Xaml.Data; | ||
using Windows.UI.Xaml.Input; | ||
using Windows.UI.Xaml.Media; | ||
using Windows.UI.Xaml.Navigation; | ||
#endif | ||
|
||
namespace Uno.Toolkit.Samples.Content.Helpers; | ||
|
||
[SamplePage(SampleCategory.Helpers, "MediaGalleryHelper", SourceSdk.Uno, IconSymbol = Symbol.BrowsePhotos, DataType = typeof(MediaGalleryHelperSampleVM))] | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public sealed partial class MediaGalleryHelperSamplePage : Page | ||
{ | ||
public MediaGalleryHelperSamplePage() | ||
{ | ||
this.InitializeComponent(); | ||
this.Loaded += (s, e) => | ||
{ | ||
if ((DataContext as Sample)?.Data is MediaGalleryHelperSampleVM vm) | ||
{ | ||
vm.XamlRoot = this.XamlRoot; | ||
} | ||
}; | ||
} | ||
} | ||
|
||
public class MediaGalleryHelperSampleVM : ViewModelBase | ||
{ | ||
public XamlRoot XamlRoot { get; set; } | ||
|
||
#if __ANDROID__ || __IOS__ | ||
public ICommand CheckAccessCommand => new Command(async (_) => | ||
{ | ||
var success = await MediaGallery.CheckAccessAsync(); | ||
await new ContentDialog | ||
{ | ||
Title = "Permission check", | ||
Content = $"Access {(success ? "granted" : "denied")}.", | ||
CloseButtonText = "OK", | ||
XamlRoot = XamlRoot | ||
}.ShowAsync(); | ||
}); | ||
|
||
public ICommand SaveCommand => new Command(async (_) => | ||
{ | ||
if (await MediaGallery.CheckAccessAsync()) | ||
{ | ||
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/UnoLogo.png", UriKind.Absolute)); | ||
using var stream = await file.OpenStreamForReadAsync(); | ||
await MediaGallery.SaveAsync(MediaFileType.Image, stream, "UnoLogo.png"); | ||
} | ||
else | ||
{ | ||
await new ContentDialog | ||
{ | ||
Title = "Permission required", | ||
Content = "The app requires access to the device's gallery to save the image.", | ||
CloseButtonText = "OK", | ||
XamlRoot = XamlRoot | ||
}.ShowAsync(); | ||
} | ||
}); | ||
|
||
public ICommand SaveRandomNameCommand => new Command(async (_) => | ||
{ | ||
if (await MediaGallery.CheckAccessAsync()) | ||
{ | ||
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/UnoLogo.png", UriKind.Absolute)); | ||
using var stream = await file.OpenStreamForReadAsync(); | ||
|
||
var fileName = Guid.NewGuid() + ".png"; | ||
await MediaGallery.SaveAsync(MediaFileType.Image, stream, fileName); | ||
} | ||
else | ||
{ | ||
await new ContentDialog | ||
{ | ||
Title = "Permission required", | ||
Content = "The app requires access to the device's gallery to save the image.", | ||
CloseButtonText = "OK", | ||
XamlRoot = XamlRoot | ||
}.ShowAsync(); | ||
} | ||
}); | ||
#endif | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
#if __IOS__ || __ANDROID__ | ||
namespace Uno.Toolkit.UI; | ||
|
||
/// <summary> | ||
/// Represents a media file type. | ||
/// </summary> | ||
public enum MediaFileType | ||
{ | ||
/// <summary> | ||
/// Image media file type. | ||
/// </summary> | ||
Image, | ||
|
||
/// <summary> | ||
/// Video media file type. | ||
/// </summary> | ||
Video, | ||
} | ||
#endif |
136 changes: 136 additions & 0 deletions
136
src/Uno.Toolkit.UI/Helpers/MediaGallery/MediaGallery.Android.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
#if __ANDROID__ | ||
using Android.App; | ||
using Android.Content; | ||
using Android.OS; | ||
using Android.Provider; | ||
using Android.Webkit; | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Windows.ApplicationModel; | ||
using Windows.Extensions; | ||
using static Android.Provider.MediaStore; | ||
using Environment = Android.OS.Environment; | ||
using File = Java.IO.File; | ||
using Path = System.IO.Path; | ||
using Stream = System.IO.Stream; | ||
using Uri = Android.Net.Uri; | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
namespace Uno.Toolkit.UI; | ||
|
||
partial class MediaGallery | ||
{ | ||
private static readonly DateTime _unixStartDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private static async Task<bool> CheckAccessPlatformAsync() | ||
{ | ||
if ((int)Build.VERSION.SdkInt < 29) | ||
{ | ||
return await PermissionsHelper.CheckWriteExternalStoragePermission(default); | ||
} | ||
else | ||
{ | ||
return true; | ||
} | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
private static async Task SavePlatformAsync(MediaFileType type, Stream sourceStream, string targetFileName) | ||
{ | ||
var context = Application.Context; | ||
var contentResolver = context.ContentResolver ?? throw new InvalidOperationException("ContentResolver is not set."); | ||
|
||
var appFolderName = Package.Current.DisplayName; | ||
// Ensure folder name is file system safe | ||
appFolderName = string.Join("_", appFolderName.Split(Path.GetInvalidFileNameChars())); | ||
|
||
var dateTimeNow = DateTime.Now; | ||
|
||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetFileName); | ||
var extension = Path.GetExtension(targetFileName).ToLower(); | ||
|
||
using var values = new ContentValues(); | ||
|
||
values.Put(IMediaColumns.DateAdded, TimeSeconds(dateTimeNow)); | ||
values.Put(IMediaColumns.Title, fileNameWithoutExtension); | ||
values.Put(IMediaColumns.DisplayName, targetFileName); | ||
|
||
var mimeTypeMap = MimeTypeMap.Singleton ?? throw new InvalidOperationException("MimeTypeMap is not set."); | ||
|
||
var mimeType = mimeTypeMap.GetMimeTypeFromExtension(extension.Replace(".", string.Empty)); | ||
if (!string.IsNullOrWhiteSpace(mimeType)) | ||
values.Put(IMediaColumns.MimeType, mimeType); | ||
|
||
using var externalContentUri = type == MediaFileType.Image | ||
? Images.Media.ExternalContentUri | ||
: Video.Media.ExternalContentUri; | ||
|
||
if (externalContentUri is null) | ||
{ | ||
throw new InvalidOperationException($"External Content URI for {type} is not available."); | ||
} | ||
|
||
var relativePath = type == MediaFileType.Image | ||
? Environment.DirectoryPictures | ||
: Environment.DirectoryMovies; | ||
|
||
if (relativePath is null) | ||
{ | ||
throw new InvalidOperationException($"Relative path for {type} is not available."); | ||
} | ||
|
||
if ((int)Build.VERSION.SdkInt >= 29) | ||
{ | ||
values.Put(IMediaColumns.RelativePath, Path.Combine(relativePath, appFolderName)); | ||
values.Put(IMediaColumns.IsPending, true); | ||
|
||
using var uri = contentResolver.Insert(externalContentUri, values); | ||
|
||
if (uri is null) | ||
{ | ||
throw new InvalidOperationException("Could not generate new content URI"); | ||
} | ||
|
||
using var stream = contentResolver.OpenOutputStream(uri); | ||
|
||
if (stream is null) | ||
{ | ||
throw new InvalidOperationException("Could not open output stream"); | ||
} | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
await sourceStream.CopyToAsync(stream); | ||
stream.Close(); | ||
|
||
values.Put(IMediaColumns.IsPending, false); | ||
context.ContentResolver.Update(uri, values, null, null); | ||
} | ||
else | ||
{ | ||
#pragma warning disable CS0618 // Type or member is obsolete | ||
using var directory = new File(Environment.GetExternalStoragePublicDirectory(relativePath), appFolderName); | ||
directory.Mkdirs(); | ||
|
||
using var file = new File(directory, targetFileName); | ||
|
||
using var fileOutputStream = System.IO.File.Create(file.AbsolutePath); | ||
await sourceStream.CopyToAsync(fileOutputStream); | ||
fileOutputStream.Close(); | ||
|
||
values.Put(IMediaColumns.Data, file.AbsolutePath); | ||
contentResolver.Insert(externalContentUri, values); | ||
|
||
#pragma warning disable CA1422 // Validate platform compatibility | ||
using var mediaScanIntent = new Intent(Intent.ActionMediaScannerScanFile); | ||
#pragma warning restore CA1422 // Validate platform compatibility | ||
mediaScanIntent.SetData(Uri.FromFile(file)); | ||
context.SendBroadcast(mediaScanIntent); | ||
#pragma warning restore CS0618 // Type or member is obsolete | ||
} | ||
} | ||
|
||
private static long TimeMillis(DateTime current) => (long)GetTimeDifference(current).TotalMilliseconds; | ||
|
||
private static long TimeSeconds(DateTime current) => (long)GetTimeDifference(current).TotalSeconds; | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private static TimeSpan GetTimeDifference(DateTime current) => current.ToUniversalTime() - _unixStartDate; | ||
} | ||
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
#if __IOS__ || __ANDROID__ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace Uno.Toolkit.UI; | ||
|
||
/// <summary> | ||
/// Allows interaction with the device's media gallery. | ||
/// </summary> | ||
public static partial class MediaGallery | ||
{ | ||
/// <summary> | ||
/// Checks the user permission to access the device's gallery. | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Will trigger the permission request if not already granted. | ||
/// </summary> | ||
/// <returns>A value indicating whether the user has access.</returns> | ||
public static async Task<bool> CheckAccessAsync() => await CheckAccessPlatformAsync(); | ||
MartinZikmund marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Saves a media file to the device's gallery. | ||
/// </summary> | ||
/// <param name="type">Media file type.</param> | ||
/// <param name="data">Byte array representing the file.</param> | ||
/// <param name="targetFileName">Target file name.</param> | ||
/// <returns>Task representing the progress of the operation.</returns> | ||
public static async Task SaveAsync(MediaFileType type, byte[] data, string targetFileName) | ||
{ | ||
using var memoryStream = new MemoryStream(data); | ||
await SaveAsync(type, memoryStream, targetFileName); | ||
} | ||
|
||
/// <summary> | ||
/// Saves a media file to the device's gallery. | ||
/// </summary> | ||
/// <param name="type">Media file type.</param> | ||
/// <param name="stream">Stream representing the file.</param> | ||
/// <param name="targetFileName">Target file name.</param> | ||
/// <returns>Task representing the progress of the operation.</returns> | ||
public static async Task SaveAsync(MediaFileType type, Stream stream, string targetFileName) => | ||
await SavePlatformAsync(type, stream, targetFileName); | ||
} | ||
#endif |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.