Merge pull request #11212 from Game4all/android-multiple-import-support-suppot

Add ability to import multiple files at once on android
This commit is contained in:
Bartłomiej Dach 2021-01-17 00:44:50 +01:00 committed by GitHub
commit 1dc2af57a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 54 additions and 39 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,13 +13,14 @@ using Android.OS;
using Android.Provider; using Android.Provider;
using Android.Views; using Android.Views;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Database;
namespace osu.Android namespace osu.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })] [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {
@ -54,43 +56,59 @@ namespace osu.Android
{ {
case Intent.ActionDefault: case Intent.ActionDefault:
if (intent.Scheme == ContentResolver.SchemeContent) if (intent.Scheme == ContentResolver.SchemeContent)
handleImportFromUri(intent.Data); handleImportFromUris(intent.Data);
else if (osu_url_schemes.Contains(intent.Scheme)) else if (osu_url_schemes.Contains(intent.Scheme))
game.HandleLink(intent.DataString); game.HandleLink(intent.DataString);
break; break;
case Intent.ActionSend: case Intent.ActionSend:
case Intent.ActionSendMultiple:
{ {
var content = intent.ClipData?.GetItemAt(0); var uris = new List<Uri>();
if (content != null) for (int i = 0; i < intent.ClipData?.ItemCount; i++)
handleImportFromUri(content.Uri); {
var content = intent.ClipData?.GetItemAt(i);
if (content != null)
uris.Add(content.Uri);
}
handleImportFromUris(uris.ToArray());
break; break;
} }
} }
} }
private void handleImportFromUri(Uri uri) => Task.Factory.StartNew(async () => private void handleImportFromUris(params Uri[] uris) => Task.Factory.StartNew(async () =>
{ {
// there are more performant overloads of this method, but this one is the most backwards-compatible var tasks = new List<ImportTask>();
// (dates back to API 1).
var cursor = ContentResolver?.Query(uri, null, null, null, null);
if (cursor == null) await Task.WhenAll(uris.Select(async uri =>
return; {
// there are more performant overloads of this method, but this one is the most backwards-compatible
// (dates back to API 1).
var cursor = ContentResolver?.Query(uri, null, null, null, null);
cursor.MoveToFirst(); if (cursor == null)
return;
var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); cursor.MoveToFirst();
string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
// OpenInputStream() seems to not necessarily be. string filename = cursor.GetString(filenameColumn);
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = ContentResolver.OpenInputStream(uri))
await stream.CopyToAsync(copy);
await game.Import(copy, filename); // SharpCompress requires archive streams to be seekable, which the stream opened by
// OpenInputStream() seems to not necessarily be.
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = ContentResolver.OpenInputStream(uri))
await stream.CopyToAsync(copy);
lock (tasks)
{
tasks.Add(new ImportTask(copy, filename));
}
}));
await game.Import(tasks.ToArray());
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
} }
} }

View File

@ -115,13 +115,13 @@ namespace osu.Game.Database
return Import(notification, paths.Select(p => new ImportTask(p)).ToArray()); return Import(notification, paths.Select(p => new ImportTask(p)).ToArray());
} }
public Task Import(Stream stream, string filename) public Task Import(params ImportTask[] tasks)
{ {
var notification = new ProgressNotification { State = ProgressNotificationState.Active }; var notification = new ProgressNotification { State = ProgressNotificationState.Active };
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
return Import(notification, new ImportTask(stream, filename)); return Import(notification, tasks);
} }
protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks) protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace osu.Game.Database namespace osu.Game.Database
@ -19,11 +18,10 @@ namespace osu.Game.Database
Task Import(params string[] paths); Task Import(params string[] paths);
/// <summary> /// <summary>
/// Import the provided stream as a simple item. /// Import the specified files from the given import tasks.
/// </summary> /// </summary>
/// <param name="stream">The stream to import files from. Should be in a supported archive format.</param> /// <param name="tasks">The import tasks from which the files should be imported.</param>
/// <param name="filename">The filename of the archive being imported.</param> Task Import(params ImportTask[] tasks);
Task Import(Stream stream, string filename);
/// <summary> /// <summary>
/// An array of accepted file extensions (in the standard format of ".abc"). /// An array of accepted file extensions (in the standard format of ".abc").

View File

@ -51,7 +51,7 @@ using osu.Game.Screens.Select;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel; using LogLevel = osu.Framework.Logging.LogLevel;
using System.IO; using osu.Game.Database;
namespace osu.Game namespace osu.Game
{ {
@ -438,10 +438,10 @@ namespace osu.Game
}, validScreens: new[] { typeof(PlaySongSelect) }); }, validScreens: new[] { typeof(PlaySongSelect) });
} }
public override Task Import(Stream stream, string filename) public override Task Import(params ImportTask[] imports)
{ {
// encapsulate task as we don't want to begin the import process until in a ready state. // encapsulate task as we don't want to begin the import process until in a ready state.
var importTask = new Task(async () => await base.Import(stream, filename)); var importTask = new Task(async () => await base.Import(imports));
waitForReady(() => this, _ => importTask.Start()); waitForReady(() => this, _ => importTask.Start());

View File

@ -419,15 +419,14 @@ namespace osu.Game
} }
} }
public virtual async Task Import(Stream stream, string filename) public virtual async Task Import(params ImportTask[] tasks)
{ {
var extension = Path.GetExtension(filename)?.ToLowerInvariant(); var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant());
await Task.WhenAll(tasksPerExtension.Select(taskGroup =>
foreach (var importer in fileImporters)
{ {
if (importer.HandledExtensions.Contains(extension)) var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
await importer.Import(stream, Path.GetFileNameWithoutExtension(filename)); return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
} }));
} }
public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);

View File

@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit.Setup
return Task.CompletedTask; return Task.CompletedTask;
} }
Task ICanAcceptFiles.Import(Stream stream, string filename) => throw new NotImplementedException(); Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
protected override void LoadComplete() protected override void LoadComplete()
{ {