diff --git a/MRETestBed/Assets/Scenes/HelloWorld.unity b/MRETestBed/Assets/Scenes/HelloWorld.unity index be0ea848..d195011c 100644 --- a/MRETestBed/Assets/Scenes/HelloWorld.unity +++ b/MRETestBed/Assets/Scenes/HelloWorld.unity @@ -222,6 +222,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 276664683} diff --git a/MRETestBed/Assets/Scenes/Standalone.unity b/MRETestBed/Assets/Scenes/Standalone.unity index 754dfb26..57f6a761 100644 --- a/MRETestBed/Assets/Scenes/Standalone.unity +++ b/MRETestBed/Assets/Scenes/Standalone.unity @@ -38,7 +38,7 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 1966163892} - m_IndirectSpecularColor: {r: 0.45135534, g: 0.5009366, b: 0.5727883, a: 1} + m_IndirectSpecularColor: {r: 0.45135576, g: 0.500937, b: 0.57278883, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: @@ -234,6 +234,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 276664683} diff --git a/MRETestBed/Assets/Scenes/SynchronizationTest-localhost.unity b/MRETestBed/Assets/Scenes/SynchronizationTest-localhost.unity index 7dc68d71..e7866ba7 100644 --- a/MRETestBed/Assets/Scenes/SynchronizationTest-localhost.unity +++ b/MRETestBed/Assets/Scenes/SynchronizationTest-localhost.unity @@ -293,6 +293,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 368773028} PlaceholderObject: {fileID: 164408130} UserGameObject: {fileID: 1767571516} @@ -517,6 +518,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 582437838} UserGameObject: {fileID: 1767571516} diff --git a/MRETestBed/Assets/Scenes/TriggerVolumeTestBed-localhost.unity b/MRETestBed/Assets/Scenes/TriggerVolumeTestBed-localhost.unity index 3c0cb566..6977219f 100644 --- a/MRETestBed/Assets/Scenes/TriggerVolumeTestBed-localhost.unity +++ b/MRETestBed/Assets/Scenes/TriggerVolumeTestBed-localhost.unity @@ -38,7 +38,7 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641258, b: 0.5748172, a: 1} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: @@ -435,6 +435,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 18576793} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 365627181} diff --git a/MRETestBed/Assets/TestBed Assets/Scripts/MREComponent.cs b/MRETestBed/Assets/TestBed Assets/Scripts/MREComponent.cs index 18c5f7b1..ea734cba 100644 --- a/MRETestBed/Assets/TestBed Assets/Scripts/MREComponent.cs +++ b/MRETestBed/Assets/TestBed Assets/Scripts/MREComponent.cs @@ -6,7 +6,7 @@ using MixedRealityExtension.API; using MixedRealityExtension.App; using MixedRealityExtension.Assets; -using MixedRealityExtension.Core.Interfaces; +using MixedRealityExtension.Core; using MixedRealityExtension.Factories; using MixedRealityExtension.PluginInterfaces; using MixedRealityExtension.RPC; @@ -62,6 +62,9 @@ public class UserProperty public bool AutoJoin = true; + [SerializeField] + private Permissions GrantedPermissions; + public Transform SceneRoot; public GameObject PlaceholderObject; @@ -131,7 +134,6 @@ void Start() defaultMaterial: DefaultPrimMaterial, layerApplicator: new SimpleLayerApplicator(0, 9, 10, 5), assetCache: assetCache, - behaviorFactory: new BehaviorFactory(), textFactory: new TmpTextFactory() { DefaultFont = DefaultFont, @@ -140,12 +142,14 @@ void Start() MonospaceFont = MonospaceFont, CursiveFont = CursiveFont }, - libraryFactory: new ResourceFactory(), - userInfoProvider: new UserInfoProvider(), + permissionManager: new SimplePermissionManager(GrantedPermissions), + behaviorFactory: new BehaviorFactory(), dialogFactory: DialogFactory, - logger: new MRELogger(), + libraryFactory: new ResourceFactory(), + gltfImporterFactory: new VertexShadedGltfImporterFactory(), materialPatcher: new VertexMaterialPatcher(), - gltfImporterFactory: new VertexShadedGltfImporterFactory() + userInfoProvider: new UserInfoProvider(), + logger: new MRELogger() ); _apiInitialized = true; } diff --git a/MRETestBed_MRTK/Assets/Scenes/HelloWorld_2D.unity b/MRETestBed_MRTK/Assets/Scenes/HelloWorld_2D.unity index 22ddd37d..b5c2f6cc 100644 --- a/MRETestBed_MRTK/Assets/Scenes/HelloWorld_2D.unity +++ b/MRETestBed_MRTK/Assets/Scenes/HelloWorld_2D.unity @@ -241,6 +241,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 276664683} @@ -252,6 +253,7 @@ MonoBehaviour: DefaultPrimMaterial: {fileID: 2100000, guid: 265cb5b418966a64ea212597d469fa33, type: 2} DialogFactory: {fileID: 114153473817070278, guid: f1f77a0b742ec0841858c35dc9348515, type: 3} + EnableMRTK: 0 --- !u!114 &1141388992 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/MRETestBed_MRTK/Assets/Scenes/Localhost_MRTK.unity b/MRETestBed_MRTK/Assets/Scenes/Localhost_MRTK.unity index 821ccdd1..b69b0f28 100644 --- a/MRETestBed_MRTK/Assets/Scenes/Localhost_MRTK.unity +++ b/MRETestBed_MRTK/Assets/Scenes/Localhost_MRTK.unity @@ -220,6 +220,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 276568010} diff --git a/MRETestBed_MRTK/Assets/Scenes/Standalone_2D.unity b/MRETestBed_MRTK/Assets/Scenes/Standalone_2D.unity index 754dfb26..44181c1b 100644 --- a/MRETestBed_MRTK/Assets/Scenes/Standalone_2D.unity +++ b/MRETestBed_MRTK/Assets/Scenes/Standalone_2D.unity @@ -38,7 +38,7 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 1966163892} - m_IndirectSpecularColor: {r: 0.45135534, g: 0.5009366, b: 0.5727883, a: 1} + m_IndirectSpecularColor: {r: 0.45135576, g: 0.500937, b: 0.57278883, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: @@ -234,6 +234,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 276664683} @@ -244,6 +245,7 @@ MonoBehaviour: CursiveFont: {fileID: 11400000, guid: b31d2923d654c0e49b3e196355c7878e, type: 2} DefaultPrimMaterial: {fileID: 2100000, guid: 265cb5b418966a64ea212597d469fa33, type: 2} DialogFactory: {fileID: 36258468} + EnableMRTK: 0 --- !u!114 &1141388992 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/MRETestBed_MRTK/Assets/Scenes/SynchronizationTest-localhost_2D.unity b/MRETestBed_MRTK/Assets/Scenes/SynchronizationTest-localhost_2D.unity index 7dc68d71..9cae3889 100644 --- a/MRETestBed_MRTK/Assets/Scenes/SynchronizationTest-localhost_2D.unity +++ b/MRETestBed_MRTK/Assets/Scenes/SynchronizationTest-localhost_2D.unity @@ -293,6 +293,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 368773028} PlaceholderObject: {fileID: 164408130} UserGameObject: {fileID: 1767571516} @@ -304,6 +305,7 @@ MonoBehaviour: DefaultPrimMaterial: {fileID: 2100000, guid: 265cb5b418966a64ea212597d469fa33, type: 2} DialogFactory: {fileID: 114153473817070278, guid: f1f77a0b742ec0841858c35dc9348515, type: 3} + EnableMRTK: 0 --- !u!4 &368773028 Transform: m_ObjectHideFlags: 0 @@ -517,6 +519,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 1141388990} PlaceholderObject: {fileID: 582437838} UserGameObject: {fileID: 1767571516} @@ -528,6 +531,7 @@ MonoBehaviour: DefaultPrimMaterial: {fileID: 2100000, guid: 265cb5b418966a64ea212597d469fa33, type: 2} DialogFactory: {fileID: 114153473817070278, guid: f1f77a0b742ec0841858c35dc9348515, type: 3} + EnableMRTK: 0 --- !u!1 &1256608368 GameObject: m_ObjectHideFlags: 0 diff --git a/MRETestBed_MRTK/Assets/Scenes/TriggerVolumeTestBed-localhost_2D.unity b/MRETestBed_MRTK/Assets/Scenes/TriggerVolumeTestBed-localhost_2D.unity index 3c0cb566..2ee07d07 100644 --- a/MRETestBed_MRTK/Assets/Scenes/TriggerVolumeTestBed-localhost_2D.unity +++ b/MRETestBed_MRTK/Assets/Scenes/TriggerVolumeTestBed-localhost_2D.unity @@ -38,7 +38,7 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641258, b: 0.5748172, a: 1} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: @@ -435,6 +435,7 @@ MonoBehaviour: UserProperties: [] AutoStart: 0 AutoJoin: 1 + GrantedPermissions: -1 SceneRoot: {fileID: 18576793} PlaceholderObject: {fileID: 0} UserGameObject: {fileID: 365627181} @@ -446,6 +447,7 @@ MonoBehaviour: DefaultPrimMaterial: {fileID: 2100000, guid: 265cb5b418966a64ea212597d469fa33, type: 2} DialogFactory: {fileID: 114153473817070278, guid: f1f77a0b742ec0841858c35dc9348515, type: 3} + EnableMRTK: 0 --- !u!1 &2044912625 GameObject: m_ObjectHideFlags: 0 diff --git a/MRETestBed_MRTK/Assets/TestBed Assets/Scripts/MREComponent.cs b/MRETestBed_MRTK/Assets/TestBed Assets/Scripts/MREComponent.cs index 747ed57d..b636225e 100644 --- a/MRETestBed_MRTK/Assets/TestBed Assets/Scripts/MREComponent.cs +++ b/MRETestBed_MRTK/Assets/TestBed Assets/Scripts/MREComponent.cs @@ -6,6 +6,7 @@ using MixedRealityExtension.API; using MixedRealityExtension.App; using MixedRealityExtension.Assets; +using MixedRealityExtension.Core; using MixedRealityExtension.Core.Interfaces; using MixedRealityExtension.Factories; using MixedRealityExtension.PluginInterfaces; @@ -63,6 +64,9 @@ public class UserProperty public bool AutoJoin = true; + [SerializeField] + private Permissions GrantedPermissions; + public Transform SceneRoot; public GameObject PlaceholderObject; @@ -135,7 +139,6 @@ void Start() defaultMaterial: DefaultPrimMaterial, layerApplicator: new SimpleLayerApplicator(0, 9, 10, 5), assetCache: assetCache, - behaviorFactory: new MRTKBehaviorFactory(), textFactory: new TmpTextFactory() { DefaultFont = DefaultFont, @@ -144,12 +147,14 @@ void Start() MonospaceFont = MonospaceFont, CursiveFont = CursiveFont }, - libraryFactory: new ResourceFactory(), - userInfoProvider: new UserInfoProvider(), + permissionManager: new SimplePermissionManager(GrantedPermissions), + behaviorFactory: new BehaviorFactory(), dialogFactory: DialogFactory, - logger: new MRELogger(), + libraryFactory: new ResourceFactory(), + gltfImporterFactory: new VertexShadedGltfImporterFactory(), materialPatcher: new VertexMaterialPatcher(), - gltfImporterFactory: new VertexShadedGltfImporterFactory() + userInfoProvider: new UserInfoProvider(), + logger: new MRELogger() ); _apiInitialized = true; } diff --git a/MREUnityRuntime/MREUnityRuntimeLib/API/MREApi.cs b/MREUnityRuntime/MREUnityRuntimeLib/API/MREApi.cs index d8e66d34..a3143663 100644 --- a/MREUnityRuntime/MREUnityRuntimeLib/API/MREApi.cs +++ b/MREUnityRuntime/MREUnityRuntimeLib/API/MREApi.cs @@ -26,43 +26,54 @@ public static class MREAPI /// The material template used for all SDK-spawned meshes. /// The class used to apply MRE layers to Unity colliders. /// The class responsible for long-term asset caching. - /// The behavior factory to use within the runtime. /// The text factory to use within the runtime. - /// The primitive factory to use within the runtime. + /// The instance responsible for presenting users with permission requests. + /// The behavior factory to use within the runtime. + /// /// The library resource factory to use within the runtime. + /// + /// The primitive factory to use within the runtime. /// The glTF loader factory. Uses default GLTFSceneImporter if omitted. /// Overrides default material property map (color and mainTexture only). - /// /// Provides appId/sessionId scoped IUserInfo instances. - /// /// The logger to be used by the MRE SDK. public static void InitializeAPI( + // required properties UnityEngine.Material defaultMaterial, ILayerApplicator layerApplicator, IAssetCache assetCache, + ITextFactory textFactory, + IPermissionManager permissionManager, + // missing features if omitted IBehaviorFactory behaviorFactory = null, - ITextFactory textFactory = null, - IPrimitiveFactory primitiveFactory = null, + IDialogFactory dialogFactory = null, ILibraryResourceFactory libraryFactory = null, + IVideoPlayerFactory videoPlayerFactory = null, + // reasonable defaults provided + IPrimitiveFactory primitiveFactory = null, IGLTFImporterFactory gltfImporterFactory = null, IMaterialPatcher materialPatcher = null, - IVideoPlayerFactory videoPlayerFactory = null, IUserInfoProvider userInfoProvider = null, - IDialogFactory dialogFactory = null, IMRELogger logger = null) { + // required properties AppsAPI.DefaultMaterial = defaultMaterial; AppsAPI.LayerApplicator = layerApplicator; AppsAPI.AssetCache = assetCache; + AppsAPI.TextFactory = textFactory; + AppsAPI.PermissionManager = permissionManager; + + // missing features if omitted AppsAPI.BehaviorFactory = behaviorFactory; - AppsAPI.TextFactory = textFactory ?? throw new ArgumentException($"{nameof(textFactory)} cannot be null"); - AppsAPI.PrimitiveFactory = primitiveFactory ?? new MWPrimitiveFactory(); + AppsAPI.DialogFactory = dialogFactory; AppsAPI.LibraryResourceFactory = libraryFactory; AppsAPI.VideoPlayerFactory = videoPlayerFactory; + + // reasonable defaults provided + AppsAPI.PrimitiveFactory = primitiveFactory ?? new MWPrimitiveFactory(); AppsAPI.GLTFImporterFactory = gltfImporterFactory ?? new GLTFImporterFactory(); AppsAPI.MaterialPatcher = materialPatcher ?? new DefaultMaterialPatcher(); AppsAPI.UserInfoProvider = userInfoProvider ?? new NullUserInfoProvider(); - AppsAPI.DialogFactory = dialogFactory; #if ANDROID_DEBUG Logger = logger ?? new UnityLogger(null); @@ -128,6 +139,8 @@ public class MREAppsAPI internal IDialogFactory DialogFactory { get; set; } + internal IPermissionManager PermissionManager { get; set; } + /// /// Creates a new mixed reality extension app and adds it to the MRE runtime. /// diff --git a/MREUnityRuntime/MREUnityRuntimeLib/App/AppManifest.cs b/MREUnityRuntime/MREUnityRuntimeLib/App/AppManifest.cs new file mode 100644 index 00000000..f146a556 --- /dev/null +++ b/MREUnityRuntime/MREUnityRuntimeLib/App/AppManifest.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using MixedRealityExtension.Core; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace MixedRealityExtension.Core +{ + /// + /// Class containing author-provided metadata about an MRE instance + /// + public class AppManifest + { + /// + /// A human-readable name for this MRE + /// + public string Name; + + /// + /// A human readable description of this MRE's behavior + /// + public string Description; + + /// + /// The MRE's author name and/or contact information + /// + public string Author; + + /// + /// The license for the MRE's source code + /// + public string License; + + /// + /// The location of the MRE's public source code + /// + public string RepositoryUrl; + + /// + /// A list of permissions required for this MRE to run + /// + public Permissions[] Permissions; + + /// + /// A list of permissions that this MRE can use, but are not required + /// + public Permissions[] OptionalPermissions; + + internal static async Task DownloadManifest(Uri manifestUri) + { + var webClient = new HttpClient(); + var response = await webClient.GetAsync(manifestUri, HttpCompletionOption.ResponseContentRead); + if (response.IsSuccessStatusCode) + { + var manifestString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(manifestString, Constants.SerializerSettings); + } + else + { + return new AppManifest(); + } + } + } +} diff --git a/MREUnityRuntime/MREUnityRuntimeLib/App/MixedRealityExtensionApp.cs b/MREUnityRuntime/MREUnityRuntimeLib/App/MixedRealityExtensionApp.cs index d417c847..1fb566bb 100644 --- a/MREUnityRuntime/MREUnityRuntimeLib/App/MixedRealityExtensionApp.cs +++ b/MREUnityRuntime/MREUnityRuntimeLib/App/MixedRealityExtensionApp.cs @@ -56,6 +56,10 @@ internal sealed class MixedRealityExtensionApp : IMixedRealityExtensionApp, ICom private enum AppState { Stopped, + /// + /// Startup has been called, but we might be waiting for permission to run. + /// + WaitingForPermission, Starting, Running } @@ -63,6 +67,9 @@ private enum AppState private AppState _appState = AppState.Stopped; private int generation = 0; + [Obsolete] + private string PlatformId; + public IMRELogger Logger { get; private set; } #region Events - Public @@ -131,6 +138,8 @@ public event MWEventHandler OnActorCreated /// public RPCChannelInterface RPCChannels { get; } + public AssetManager AssetManager => _assetManager; + #endregion #region Properties - Internal @@ -151,7 +160,7 @@ public event MWEventHandler OnActorCreated internal AssetLoader AssetLoader => _assetLoader; - public AssetManager AssetManager => _assetManager; + internal Permissions GrantedPermissions = Permissions.None; #endregion @@ -220,10 +229,47 @@ private void OnRigidBodyRemoved(Guid id) } /// - public void Startup(string url, string sessionId, string platformId) + public async void Startup(string url, string sessionId, string platformId) { + if (_appState != AppState.Stopped) + { + Shutdown(); + } + ServerUri = new Uri(url, UriKind.Absolute); ServerAssetUri = new Uri(Regex.Replace(ServerUri.AbsoluteUri, "^ws(s?):", "http$1:")); + SessionId = sessionId; + PlatformId = platformId; + + _appState = AppState.WaitingForPermission; + + // download manifest + var manifestUri = new Uri(ServerAssetUri, "./manifest.json"); + var manifest = await AppManifest.DownloadManifest(manifestUri); + var neededFlags = Permissions.Execution | (manifest.Permissions?.ToFlags() ?? Permissions.None); + var wantedFlags = manifest.OptionalPermissions?.ToFlags() ?? Permissions.None; + + // get permission to run from host app + var grantedPerms = await MREAPI.AppsAPI.PermissionManager.PromptForPermissions( + appLocation: ServerUri, + permissionsNeeded: new HashSet(manifest.Permissions ?? new Permissions[0]) { Permissions.Execution }, + permissionsWanted: manifest.OptionalPermissions, + permissionFlagsNeeded: neededFlags, + permissionFlagsWanted: wantedFlags, + appManifest: manifest); + + // only use permissions that are requested, even if the user offers more + GrantedPermissions = grantedPerms & (neededFlags | wantedFlags); + + MREAPI.AppsAPI.PermissionManager.OnPermissionDecisionsChanged += OnPermissionsUpdated; + + if (!grantedPerms.HasFlag(Permissions.Execution)) + { + Debug.LogError($"User has denied permission for the MRE '{ServerUri}' to run"); + return; + } + + _appState = AppState.Starting; if (UsePhysicsBridge) { @@ -232,31 +278,21 @@ public void Startup(string url, string sessionId, string platformId) _actorManager.RigidBodyKinematicsChanged += OnRigidBodyKinematicsChanged; _actorManager.RigidBodyOwnerChanged += OnRigidBodyOwnerChanged; } + + var connection = new WebSocket(); + connection.Url = url; + connection.Headers.Add(Constants.SessionHeader, SessionId); + connection.Headers.Add(Constants.PlatformHeader, PlatformId); + connection.Headers.Add(Constants.LegacyProtocolVersionHeader, $"{Constants.LegacyProtocolVersion}"); + connection.Headers.Add(Constants.CurrentClientVersionHeader, Constants.CurrentClientVersion); + connection.Headers.Add(Constants.MinimumSupportedSDKVersionHeader, Constants.MinimumSupportedSDKVersion); + connection.OnConnecting += Conn_OnConnecting; + connection.OnConnectFailed += Conn_OnConnectFailed; + connection.OnConnected += Conn_OnConnected; + connection.OnDisconnected += Conn_OnDisconnected; + connection.OnError += Connection_OnError; + _conn = connection; - if (_conn == null) - { - if (_appState == AppState.Stopped) - { - _appState = AppState.Starting; - } - - SessionId = sessionId; - - var connection = new WebSocket(); - - connection.Url = url; - connection.Headers.Add(Constants.SessionHeader, sessionId); - connection.Headers.Add(Constants.PlatformHeader, platformId); - connection.Headers.Add(Constants.LegacyProtocolVersionHeader, $"{Constants.LegacyProtocolVersion}"); - connection.Headers.Add(Constants.CurrentClientVersionHeader, Constants.CurrentClientVersion); - connection.Headers.Add(Constants.MinimumSupportedSDKVersionHeader, Constants.MinimumSupportedSDKVersion); - connection.OnConnecting += Conn_OnConnecting; - connection.OnConnectFailed += Conn_OnConnectFailed; - connection.OnConnected += Conn_OnConnected; - connection.OnDisconnected += Conn_OnDisconnected; - connection.OnError += Connection_OnError; - _conn = connection; - } _conn.Open(); } @@ -266,6 +302,15 @@ private void OnRigidBodyOwnerChanged(Guid id, Guid? owner) _physicsBridge.setRigidBodyOwnership(id, isOwner); } + private void OnPermissionsUpdated(Uri serverUri, Permissions oldPermissions, Permissions newPermissions) + { + if (serverUri.GetLeftPart(UriPartial.Path) == ServerUri.GetLeftPart(UriPartial.Path) + && _appState != AppState.Stopped) + { + Startup(ServerUri.ToString(), SessionId, PlatformId); + } + } + /// private void Disconnect() { @@ -300,6 +345,8 @@ public void Shutdown() Disconnect(); FreeResources(); + MREAPI.AppsAPI.PermissionManager.OnPermissionDecisionsChanged -= OnPermissionsUpdated; + if (_appState != AppState.Stopped) { _appState = AppState.Stopped; @@ -396,6 +443,13 @@ public void UserJoin(GameObject userGO, IUserInfo userInfo) { void PerformUserJoin() { + // only join the user if required + if (!GrantedPermissions.HasFlag(Permissions.UserInteraction) + && !GrantedPermissions.HasFlag(Permissions.UserTracking)) + { + return; + } + var user = userGO.GetComponents() .FirstOrDefault(_user => _user.AppInstanceId == this.InstanceId); @@ -995,6 +1049,14 @@ private void OnShowDialog(ShowDialog payload, Action onCompleteCallback) ); onCompleteCallback?.Invoke(); } + else if (!GrantedPermissions.HasFlag(Permissions.UserInteraction)) + { + Protocol.Send( + new DialogResponse() { FailureMessage = "The user has refused the MRE permission to open dialogs" }, + payload.MessageId + ); + onCompleteCallback?.Invoke(); + } else { MREAPI.AppsAPI.DialogFactory.ShowDialog(this, payload.Text, payload.AcceptInput, (submitted, text) => diff --git a/MREUnityRuntime/MREUnityRuntimeLib/Core/Actor.cs b/MREUnityRuntime/MREUnityRuntimeLib/Core/Actor.cs index d710aa45..8003c714 100644 --- a/MREUnityRuntime/MREUnityRuntimeLib/Core/Actor.cs +++ b/MREUnityRuntime/MREUnityRuntimeLib/Core/Actor.cs @@ -777,7 +777,8 @@ private bool PerformAttach() DetachFromAttachPointParent(); IUserInfo userInfo = MREAPI.AppsAPI.UserInfoProvider.GetUserInfo(App, Attachment.UserId); - if (userInfo != null) + if (userInfo != null && + (Attachment.UserId != App.LocalUser?.Id || App.GrantedPermissions.HasFlag(Permissions.UserInteraction))) { userInfo.BeforeAvatarDestroyed -= UserInfo_BeforeAvatarDestroyed; diff --git a/MREUnityRuntime/MREUnityRuntimeLib/Core/Permissions.cs b/MREUnityRuntime/MREUnityRuntimeLib/Core/Permissions.cs new file mode 100644 index 00000000..9f9a1fd8 --- /dev/null +++ b/MREUnityRuntime/MREUnityRuntimeLib/Core/Permissions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; + +namespace MixedRealityExtension.Core +{ + [Flags] + public enum Permissions : long + { + None = 0, + Execution = 1, + UserTracking = 2, + UserInteraction = 4 + } + + public static class PermissionsExtensions + { + /// + /// Convenience method to convert an enumerable of permissions into a bitfield + /// + /// + /// + public static Permissions ToFlags(this IEnumerable enumerable) + { + var aggregate = Permissions.None; + foreach (var perm in enumerable) + { + aggregate |= perm; + } + return aggregate; + } + + /// + /// Convenience method to convert a bitfield of Permissions into an enumerable + /// + /// + /// + public static IEnumerable ToEnumerable(this Permissions flags) + { + var allPerms = Enum.GetValues(typeof(Permissions)); + var aggregate = new List(allPerms.Length); + foreach (Permissions perm in allPerms) + { + if (flags.HasFlag(perm)) + { + aggregate.Add(perm); + } + } + return aggregate; + } + } +} diff --git a/MREUnityRuntime/MREUnityRuntimeLib/Factories/SimplePermissionsManager.cs b/MREUnityRuntime/MREUnityRuntimeLib/Factories/SimplePermissionsManager.cs new file mode 100644 index 00000000..e370b69b --- /dev/null +++ b/MREUnityRuntime/MREUnityRuntimeLib/Factories/SimplePermissionsManager.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using MixedRealityExtension.Core; +using MixedRealityExtension.PluginInterfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MixedRealityExtension.Factories +{ + /// + /// Simple permission manager that grants a fixed set of permissions to all MREs + /// + public class SimplePermissionManager : IPermissionManager + { + /// + /// The static set of permissions that this manager grants + /// + public Permissions GrantedPermissions { get; private set; } + + /// + /// Set up the simple manager + /// + /// The permissions to grant to all MREs + public SimplePermissionManager(Permissions grantedPermissions) + { + GrantedPermissions = grantedPermissions; + } + + /// + public event Action OnPermissionDecisionsChanged; + + /// + public Task PromptForPermissions( + Uri appLocation, + IEnumerable permissionsNeeded, + IEnumerable permissionsWanted, + Permissions permissionFlagsNeeded, + Permissions permissionFlagsWanted, + AppManifest appManifest) + { + return Task.FromResult(GrantedPermissions); + } + + /// + public Permissions CurrentPermissions(Uri appLocation) + { + return GrantedPermissions; + } + } +} diff --git a/MREUnityRuntime/MREUnityRuntimeLib/MREUnityRuntimeLib.csproj b/MREUnityRuntime/MREUnityRuntimeLib/MREUnityRuntimeLib.csproj index 5d8044c8..ea0b5d6e 100644 --- a/MREUnityRuntime/MREUnityRuntimeLib/MREUnityRuntimeLib.csproj +++ b/MREUnityRuntime/MREUnityRuntimeLib/MREUnityRuntimeLib.csproj @@ -111,6 +111,7 @@ + @@ -125,6 +126,7 @@ + @@ -136,6 +138,7 @@ + @@ -150,6 +153,7 @@ + diff --git a/MREUnityRuntime/MREUnityRuntimeLib/PluginInterfaces/IPermissionManager.cs b/MREUnityRuntime/MREUnityRuntimeLib/PluginInterfaces/IPermissionManager.cs new file mode 100644 index 00000000..3d0ea408 --- /dev/null +++ b/MREUnityRuntime/MREUnityRuntimeLib/PluginInterfaces/IPermissionManager.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using MixedRealityExtension.Core; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MixedRealityExtension.PluginInterfaces +{ + /// + /// Permission management interface for MRE host apps. Supports both IEnumerable and bitfield-based permission lists. + /// + public interface IPermissionManager + { + /// + /// Request permissions from the user, and return a Task that resolves with those permissions the user has granted. + /// + /// The URI of the MRE requesting permission. + /// An enumerable of the permissions required for the MRE to run. + /// An enumerable of the permissions the MRE can use, but are not required. + /// Same as permissionsNeeded, but in a bitfield. + /// Same as permissionsWanted, but in a bitfield. + /// The full app manifest, which includes enumerations of the required and optional permissions. + /// + Task PromptForPermissions( + Uri appLocation, + IEnumerable permissionsNeeded, + IEnumerable permissionsWanted, + Permissions permissionFlagsNeeded, + Permissions permissionFlagsWanted, + AppManifest appManifest); + + /// + /// Get the currently granted permissions for the MRE origin without requesting new ones. + /// + /// The URI of the MRE that you want to know about. + /// A bitfield of the currently granted permissions for the given MRE. + Permissions CurrentPermissions(Uri appLocation); + + /// + /// Event that is fired when any permissions are edited. Receives as arguments the app location URI, the old + /// permission set, and the new permission set. + /// + event Action OnPermissionDecisionsChanged; + } +}