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

Feature - custom albums #123

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -349,4 +349,7 @@ obj/
packages/

#Android Resource designer
Resource.designer.cs
Resource.designer.cs

#Ignore macOS .DS_Store files.
.DS_Store
6 changes: 3 additions & 3 deletions MediaGallery/MediaGallery/MediaGallery.netstandard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ public static partial class MediaGallery
static Task<IEnumerable<IMediaFile>> PlatformPickAsync(MediaPickRequest request, CancellationToken token)
=> Task.FromResult<IEnumerable<IMediaFile>>(null);

static Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName)
static Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null)
=> Task.CompletedTask;

static Task PlatformSaveAsync(MediaFileType type, string filePath)
static Task PlatformSaveAsync(MediaFileType type, string filePath, string albumName = null)
=> Task.CompletedTask;

static Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName)
static Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null)
=> Task.CompletedTask;

static bool PlatformCheckCapturePhotoSupport()
Expand Down
18 changes: 10 additions & 8 deletions MediaGallery/MediaGallery/MediaGallery.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,40 @@ public static async Task<MediaPickResult> PickAsync(MediaPickRequest request, Ca
/// <param name="type">Type of media file to save.</param>
/// <param name="fileStream">The stream to output the file to.</param>
/// <param name="fileName">The name of the saved file including the extension.</param>
/// <param name="albumName">The name of the album to save the file to. Null for default behaviour, empty string for no album, anything else to create/use an album.</param>
/// <returns>A task representing the asynchronous save operation.</returns>
public static async Task SaveAsync(MediaFileType type, Stream fileStream, string fileName)
public static async Task SaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null)
{
await CheckPossibilitySave();

if (fileStream == null)
throw new ArgumentNullException(nameof(fileStream));
CheckFileName(fileName);

await PlatformSaveAsync(type, fileStream, fileName).ConfigureAwait(false);
await PlatformSaveAsync(type, fileStream, fileName, albumName).ConfigureAwait(false);
}

/// <param name="data">A byte array to save to the file.</param>
/// <inheritdoc cref = "SaveAsync(MediaFileType, Stream, string)" path=""/>
public static async Task SaveAsync(MediaFileType type, byte[] data, string fileName)
/// <inheritdoc cref = "SaveAsync(MediaFileType, Stream, string, string?)" path=""/>
public static async Task SaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null)
{
await CheckPossibilitySave();
if (!(data?.Length > 0))
throw new ArgumentNullException(nameof(data));
CheckFileName(fileName);

await PlatformSaveAsync(type, data, fileName).ConfigureAwait(false);
await PlatformSaveAsync(type, data, fileName, albumName).ConfigureAwait(false);
}

/// <param name="filePath">Full path to a local file.</param>
/// <inheritdoc cref = "SaveAsync(MediaFileType, Stream, string)" path=""/>
public static async Task SaveAsync(MediaFileType type, string filePath)
/// <inheritdoc cref = "SaveAsync(MediaFileType, Stream, string, string?)" path=""/>
public static async Task SaveAsync(MediaFileType type, string filePath, string albumName = null)
{
await CheckPossibilitySave();
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
throw new ArgumentException(nameof(filePath));

await PlatformSaveAsync(type, filePath).ConfigureAwait(false);
await PlatformSaveAsync(type, filePath, albumName).ConfigureAwait(false);
}

/// <summary>Checks camera support on a device</summary>
Expand Down
15 changes: 9 additions & 6 deletions MediaGallery/MediaGallery/SaveMedia.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ namespace NativeMedia
{
public static partial class MediaGallery
{
static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName)
static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null)
{
using var ms = new MemoryStream(data);
await PlatformSaveAsync(type, ms, fileName).ConfigureAwait(false);
await PlatformSaveAsync(type, ms, fileName, albumName).ConfigureAwait(false);
}

static async Task PlatformSaveAsync(MediaFileType type, string filePath)
static async Task PlatformSaveAsync(MediaFileType type, string filePath, string albumName = null)
{
using var fileStream = System.IO.File.OpenRead(filePath);
await PlatformSaveAsync(type, fileStream, Path.GetFileName(filePath)).ConfigureAwait(false);
await PlatformSaveAsync(type, fileStream, Path.GetFileName(filePath), albumName).ConfigureAwait(false);
}

static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName)
static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null)
{
var albumName = AppInfo.Name;
if (albumName == null)
{
albumName = AppInfo.Name;
}

var context = Platform.AppActivity;
var dateTimeNow = DateTime.Now;
Expand Down
65 changes: 60 additions & 5 deletions MediaGallery/MediaGallery/SaveMedia.ios.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Xml;
using Foundation;
using Photos;

namespace NativeMedia
{
public static partial class MediaGallery
{
static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName)
static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null)
{
string filePath = null;

Expand All @@ -17,15 +18,15 @@ static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string file
filePath = GetFilePath(fileName);
await File.WriteAllBytesAsync(filePath, data);

await PlatformSaveAsync(type, filePath).ConfigureAwait(false);
await PlatformSaveAsync(type, filePath, albumName).ConfigureAwait(false);
}
finally
{
DeleteFile(filePath);
}
}

static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName)
static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null)
{
string filePath = null;

Expand All @@ -36,26 +37,80 @@ static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, strin
await fileStream.CopyToAsync(stream);
stream.Close();

await PlatformSaveAsync(type, filePath).ConfigureAwait(false);
await PlatformSaveAsync(type, filePath, albumName).ConfigureAwait(false);
}
finally
{
DeleteFile(filePath);
}
}

static async Task PlatformSaveAsync(MediaFileType type, string filePath)
static async Task PlatformSaveAsync(MediaFileType type, string filePath, string albumName = null)
{
using var fileUri = new NSUrl(filePath);

PHAssetCollection collection = null;
// If albumName is null we do what we always used to do and not create an album.
// If albumName is an empty string we don't wish to create an album (which is the same as null for iOS)
// Otherwise we wish to create an album.
if (String.IsNullOrEmpty(albumName) == false)
beeradmoore marked this conversation as resolved.
Show resolved Hide resolved
{
// Fetch album.
var fetchOptions = new PHFetchOptions()
{
Predicate = NSPredicate.FromFormat($"title=\"{albumName}\"")
};
collection = PHAssetCollection.FetchAssetCollections(PHAssetCollectionType.Album, PHAssetCollectionSubtype.AlbumRegular, fetchOptions).FirstObject as PHAssetCollection;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we request additional permissions? On which versions of iOS have you tested this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I am aware no additional permissions are required to make an album. Our production app has only had NSPhotoLibraryUsageDescription in it for about 7 years.

It was only until we moved to using Xamarin.MediaGallery did we even realise there was a new NSPhotoLibraryAddUsageDescription permission.

I have only been testing the new code on iOS 16.5, but the source where I originally wrote would have been at least iOS 10 and up.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I specify the album name, it asks for additional permission. I don't really like this behavior. It will not be clear to users. Why should the user be able to select the photos to which he provides access when saving the photo?

2023-08-10.10.56.08.PM.mov

beeradmoore marked this conversation as resolved.
Show resolved Hide resolved

// Album does not exist, create it.
if (collection == null)
{
collection = await PhotoLibraryCreateAlbum(albumName).ConfigureAwait(false);
}
}

await PhotoLibraryPerformChanges(() =>
{
using var request = type == MediaFileType.Video
? PHAssetChangeRequest.FromVideo(fileUri)
: PHAssetChangeRequest.FromImage(fileUri);

// If we have a collection we should put the asset into it.
if (collection != null)
{
var assetCollectionChangeRequest = PHAssetCollectionChangeRequest.ChangeRequest(collection);
assetCollectionChangeRequest.AddAssets(new PHObject[] { request.PlaceholderForCreatedAsset });
}

}).ConfigureAwait(false);
}

static async Task<PHAssetCollection> PhotoLibraryCreateAlbum(string albumName)
{
var tcs = new TaskCompletionSource<PHAssetCollection>(TaskCreationOptions.RunContinuationsAsynchronously);

PHObjectPlaceholder placeholderForCreatedAssetCollection = null;
PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() =>
{
var createAlbum = PHAssetCollectionChangeRequest.CreateAssetCollection(albumName);
placeholderForCreatedAssetCollection = createAlbum.PlaceholderForCreatedAssetCollection;
}, (bool success, NSError error) =>
{
if (success)
{
var collectionFetchResult = PHAssetCollection.FetchAssetCollections(new string[] { placeholderForCreatedAssetCollection.LocalIdentifier }, null);
var newCollection = collectionFetchResult.FirstObject as PHAssetCollection;
beeradmoore marked this conversation as resolved.
Show resolved Hide resolved
tcs.TrySetResult(newCollection);
}
else
{
tcs.TrySetResult(null);
}
});

return await tcs.Task;
}

static async Task PhotoLibraryPerformChanges(Action action)
{
var tcs = new TaskCompletionSource<Exception>(TaskCreationOptions.RunContinuationsAsynchronously);
Expand Down
15 changes: 12 additions & 3 deletions Sample/Common/src/ViewModels/SaveVM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public SaveVM()

public ICommand SaveVideoCommand { get; }

public string AlbumName { get; set; }


async void Save(MediaFileType type, string name)
{
Expand All @@ -57,22 +59,29 @@ async void Save(MediaFileType type, string name)
{
using var stream = EmbeddedResourceProvider.Load(name);

// Note on the albumName parameter.
// If the albumName parameter is an empty string no album will be created but it will be just saved into photos/gallery.
// If the albumName parameter is a string then an album by that name will created and/or used.
// If the albumName parameter is null we use the behaviour from Xamarin.MediaGallery v2.2.1 (app name as album for Android, no album for iOS)
// In this sample we do not allow it to be null.
var albumName = AlbumName ?? String.Empty;

if (FromStream)
{
await MediaGallery.SaveAsync(type, stream, name);
await MediaGallery.SaveAsync(type, stream, name, albumName);
}
else if (FromByteArray)
{
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);

await MediaGallery.SaveAsync(type, memoryStream.ToArray(), name);
await MediaGallery.SaveAsync(type, memoryStream.ToArray(), name, albumName);
}
else if (FromCacheDirectory)
{
var filePath = await FilesHelper.SaveToCacheAsync(stream, name);

await MediaGallery.SaveAsync(type, filePath);
await MediaGallery.SaveAsync(type, filePath, albumName);
}

await DisplayAlertAsync("Save Completed Successfully");
Expand Down
4 changes: 4 additions & 0 deletions Sample/MAUI/Views/SavePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<RadioButton Content="Stream" IsChecked="{Binding FromStream}" />
<RadioButton Content="ByteArray" IsChecked="{Binding FromByteArray}" />
<RadioButton Content="CacheDirectory" IsChecked="{Binding FromCacheDirectory}" />
<StackLayout Orientation="Horizontal">
<Label Text="Save to album:" VerticalOptions="Center" />
<Entry Text="{Binding AlbumName}" Placeholder="Leave blank for no album" VerticalOptions="Center" />
</StackLayout>
</StackLayout>

<Grid Grid.Row="1" Margin="0" >
Expand Down
4 changes: 4 additions & 0 deletions Sample/Xamarin/Sample/Views/SavePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<RadioButton Content="Stream" IsChecked="{Binding FromStream}" />
<RadioButton Content="ByteArray" IsChecked="{Binding FromByteArray}" />
<RadioButton Content="CacheDirectory" IsChecked="{Binding FromCacheDirectory}" />
<StackLayout Orientation="Horizontal">
<Label Text="Save to album:" VerticalOptions="Center" />
<Entry Text="{Binding AlbumName}" Placeholder="Leave blank for no album" VerticalOptions="Center" />
</StackLayout>
</StackLayout>

<Grid Grid.Row="1" Margin="0" >
Expand Down