Skip to content
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

feat: MediaGallery #1146

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
MartinZikmund marked this conversation as resolved.
Show resolved Hide resolved
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Content\Helpers\BindingExtensionsSamplePage.xaml.cs">
<DependentUpon>BindingExtensionsSamplePage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Content\Helpers\MediaGalleryHelperSamplePage.xaml.cs">
<DependentUpon>MediaGalleryHelperSamplePage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Content\Helpers\ResponsiveExtensionsSamplePage.xaml.cs">
<DependentUpon>ResponsiveExtensionsSamplePage.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -316,6 +319,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Content\Helpers\MediaGalleryHelperSamplePage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Content\Helpers\ResponsiveExtensionsSamplePage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/iconapp.appiconset</string>

<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app would like to save photos to your gallery</string>
<!--
Adjust this to your application's encryption usage.
<key>ITSAppUsesNonExemptEncryption</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
<string>Assets.xcassets/iconapp.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>

<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app would like to save photos to your gallery</string>
<!--
Adjust this to your application's encryption usage.
<key>ITSAppUsesNonExemptEncryption</key>
Expand Down
2 changes: 2 additions & 0 deletions src/Uno.Toolkit.RuntimeTests/Tests/NavigationBarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ public async Task MainCommand_Works_From_Code_Init()
await UnitTestsUIContentHelper.WaitForIdle();

var page = frame.Content as LabelTitlePage;
#if HAS_UNO
page?.FindChild<NavigationBar>()?.MainCommand.RaiseClick();
#endif

Assert.IsTrue(success);
}
Expand Down
19 changes: 19 additions & 0 deletions src/Uno.Toolkit.UI/Helpers/MediaGallery/MediaFileType.cs
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 src/Uno.Toolkit.UI/Helpers/MediaGallery/MediaGallery.Android.cs
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
46 changes: 46 additions & 0 deletions src/Uno.Toolkit.UI/Helpers/MediaGallery/MediaGallery.cs
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
Loading
Loading