diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 7e250dce0e..e801c2ca6e 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -1,18 +1,26 @@
 // 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.
 
+using System.IO;
+using System.Threading.Tasks;
 using Android.App;
+using Android.Content;
 using Android.Content.PM;
+using Android.Net;
 using Android.OS;
+using Android.Provider;
 using Android.Views;
 using osu.Framework.Android;
 
 namespace osu.Android
 {
     [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
+    [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")]
     public class OsuGameActivity : AndroidGameActivity
     {
-        protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
+        private OsuGameAndroid game;
+
+        protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
 
         protected override void OnCreate(Bundle savedInstanceState)
         {
@@ -23,8 +31,58 @@ namespace osu.Android
 
             base.OnCreate(savedInstanceState);
 
+            // OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack.
+            // on first launch we still have to fire manually.
+            // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
+            handleIntent(Intent);
+
             Window.AddFlags(WindowManagerFlags.Fullscreen);
             Window.AddFlags(WindowManagerFlags.KeepScreenOn);
         }
+
+        protected override void OnNewIntent(Intent intent) => handleIntent(intent);
+
+        private void handleIntent(Intent intent)
+        {
+            switch (intent.Action)
+            {
+                case Intent.ActionDefault:
+                    if (intent.Scheme == ContentResolver.SchemeContent)
+                        handleImportFromUri(intent.Data);
+                    break;
+
+                case Intent.ActionSend:
+                {
+                    var content = intent.ClipData?.GetItemAt(0);
+                    if (content != null)
+                        handleImportFromUri(content.Uri);
+                    break;
+                }
+            }
+        }
+
+        private void handleImportFromUri(Uri uri) => Task.Factory.StartNew(async () =>
+        {
+            // 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);
+
+            if (cursor == null)
+                return;
+
+            cursor.MoveToFirst();
+
+            var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
+            string filename = cursor.GetString(filenameColumn);
+
+            // 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);
+
+            await game.Import(copy, filename);
+        }, TaskCreationOptions.LongRunning);
     }
 }
diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs
index c162f066d4..9af0047137 100644
--- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs
+++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
         /// Signal that the host of the room has changed.
         /// </summary>
         /// <param name="userId">The user ID of the new host.</param>
-        Task HostChanged(long userId);
+        Task HostChanged(int userId);
 
         /// <summary>
         /// Signals that the settings for this room have changed.
@@ -45,7 +45,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
         /// </summary>
         /// <param name="userId">The ID of the user performing a state change.</param>
         /// <param name="state">The new state of the user.</param>
-        Task UserStateChanged(long userId, MultiplayerUserState state);
+        Task UserStateChanged(int userId, MultiplayerUserState state);
 
         /// <summary>
         /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point.
diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs
index f1b3daf7d3..12dfe481c4 100644
--- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs
+++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Online.RealtimeMultiplayer
         /// <param name="userId">The new user which is to become host.</param>
         /// <exception cref="NotHostException">A user other than the current host is attempting to transfer host.</exception>
         /// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
-        Task TransferHost(long userId);
+        Task TransferHost(int userId);
 
         /// <summary>
         /// As the host, update the settings of the currently joined room.
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index bb638bcf3a..d67d790ce2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -52,6 +52,7 @@ using osu.Game.Updater;
 using osu.Game.Utils;
 using LogLevel = osu.Framework.Logging.LogLevel;
 using osu.Game.Users;
+using System.IO;
 
 namespace osu.Game
 {
@@ -426,6 +427,16 @@ namespace osu.Game
             }, validScreens: new[] { typeof(PlaySongSelect) });
         }
 
+        public override Task Import(Stream stream, string filename)
+        {
+            // 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));
+
+            waitForReady(() => this, _ => importTask.Start());
+
+            return importTask;
+        }
+
         protected virtual Loader CreateLoader() => new Loader();
 
         protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 0fc2b8d1d7..150569f1dd 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -395,7 +395,7 @@ namespace osu.Game
             }
         }
 
-        public async Task Import(Stream stream, string filename)
+        public virtual async Task Import(Stream stream, string filename)
         {
             var extension = Path.GetExtension(filename)?.ToLowerInvariant();
 
diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
index 6a04a95040..0a1de461ea 100644
--- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Skinning
                         break;
 
                     case "HitPosition":
-                        currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
+                        currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
                         break;
 
                     case "LightPosition":