Change the Device Preset of the Device Simulator from an editor script?

I’m trying to automate testing my adaptive UI resizing. I want to make a script that cycles through device presets, then changes the UI view, then cycles through the devices again.

I can’t find a way to access that functionality, or even the current device from the ‘UnityEditor.DeviceSimulation’ namespace.

I think I might be missing some needed understanding about how to access editor windows? maybe?

Or is it impossible because the needed methods are marked as internal in Device Simulator?

Does anyone have any ideas on how I might proceed?

1 Like

I found your question while was searching for the same answer. Since I wrote the solution, I will share it here.

In our project I’m using Reflection to access DeviceSimulator internals and manipulate them. I wrote a class that allows you to quickly select most quirky and most common devices from hotbar in Scene View and also you can iterate through all of them. I’m pretty sure you’ll find everything you need in my solution.
“using Sirenix.Utilities” dependency is not required at all, you can delete it and ditch an Expand call, no worries, it’s just for UI. I tested this code using 2021.3.27f1

Here how it looks in Scene View:

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Sirenix.Utilities;
using UnityEditor;
using UnityEngine;

namespace ACME.Tools.DeviceSimulatorHelper {
    [InitializeOnLoad]
    public sealed class DeviceSimulatorHelper {
        private const float AreaWidth = 270f;
        private const float AreaHeight = 20f;
       
        private static readonly List<SimulatorDeviceInHotBar> _deviceInHotBars = new() {
            new() {
                FriendlyName = "Samsung Galaxy Z Fold2 5G (Phone)",
                DisplayInHotBarName = "Fold 1"
            },
            new() {
                FriendlyName = "Samsung Galaxy Z Fold2 5G (Tablet)",
                DisplayInHotBarName = "Fold 2"
            },
            new() {
                FriendlyName = "Apple iPhone 13 Pro Max",
                DisplayInHotBarName = "iPhone"
            },
            new() {
                FriendlyName = "Google Pixel 2 XL",
                DisplayInHotBarName = "16:9"
            },
        };
        private static bool _isSimulatorDeviceIndexesWarmedUp;
       
        private static Lazy<Assembly> DeviceSimulatorAssembly = new(() => AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == "UnityEditor.DeviceSimulatorModule"));
        private static Lazy<Type> SimulatorWindowType => new(() = DeviceSimulatorAssembly.Value.GetType("UnityEditor.DeviceSimulation.SimulatorWindow"));
        private static Lazy<FieldInfo> SimulatorWindowMainField = new(() => SimulatorWindowType.Value.GetField("m_Main", BindingFlags.Instance | BindingFlags.NonPublic));
        private static Lazy<Type> DeviceSimulatorMainType = new(() => DeviceSimulatorAssembly.Value.GetType("UnityEditor.DeviceSimulation.DeviceSimulatorMain"));
        private static Lazy<Type> DeviceInfoAssetType = new(() => DeviceSimulatorAssembly.Value.GetType("UnityEditor.DeviceSimulation.DeviceInfoAsset"));
        private static Lazy<Type> DeviceInfoType = new(() => DeviceSimulatorAssembly.Value.GetType("UnityEditor.DeviceSimulation.DeviceInfo"));
        private static Lazy<FieldInfo> DeviceSimulatorMainDeviceIndexField = new(() => DeviceSimulatorMainType.Value.GetField("m_DeviceIndex", BindingFlags.Instance | BindingFlags.NonPublic));
        private static Lazy<PropertyInfo> DeviceSimulatorMainDeviceIndexProperty = new(() => DeviceSimulatorMainType.Value.GetProperty("deviceIndex", BindingFlags.Instance | BindingFlags.Public));
        private static Lazy<FieldInfo> DeviceSimulatorMainDevicesField = new(() => DeviceSimulatorMainType.Value.GetField("m_Devices", BindingFlags.Instance | BindingFlags.NonPublic));

        private static Lazy<FieldInfo> DeviceInfoAssetDeviceInfoField = new(() => DeviceInfoAssetType.Value.GetField("deviceInfo", BindingFlags.Instance | BindingFlags.Public));
        private static Lazy<FieldInfo> DeviceInfoFriendlyNameField = new(() => DeviceInfoType.Value.GetField("friendlyName", BindingFlags.Instance | BindingFlags.Public));

        private static Lazy<GUIStyle> SelectedButtonStyle = new(() => new GUIStyle(GUI.skin.button) {
            normal = {
                textColor = new Color(0.49f, 0.81f, 0.5f),
            },
            hover = {
                textColor = new Color(0.55f, 0.87f, 0.56f),
            },
        });

        private static float _sceneWindowHeight;
        private static Lazy<Type> GameViewType = new(() => {
            var result = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(assembly => assembly.GetTypes())
                .FirstOrDefault(type => type.Namespace == "UnityEditor" && type.Name == "PlayModeView");

            if (result == null) {
                throw new Exception("UnityEditor.PlayModeView does NOT exist in current version.");
            }

            return result;
        });
       
        private static EditorWindow _activeGameView;
        private static EditorWindow ActiveGameView {
            get {
                if (_activeGameView != null && _activeGameView.hasFocus) {
                    return _activeGameView;
                }

                var gameViews = Resources.FindObjectsOfTypeAll(GameViewType.Value);
                foreach (var gameView in gameViews) {
                    var current = gameView as EditorWindow;
                    if (current != null && current.hasFocus) {
                        _activeGameView = current;
                        return _activeGameView;
                    }
                }

                return null;
            }
        }

        static DeviceSimulatorHelper() {
            SceneView.duringSceneGui += OnSceneGUI;
        }

        private static void OnSceneGUI(SceneView sceneView) {
            if (!IsInSimulatorState()) {
                return;
            }

            WarmUpSimulatorDeviceIndexes();

            Handles.BeginGUI();
           
            EditorGUILayout.BeginVertical();
            GUILayout.FlexibleSpace();
            EditorGUILayout.EndVertical();
            var scale = GUILayoutUtility.GetLastRect();
            if (scale.height > 1f) {
                _sceneWindowHeight = scale.height;
            }

            var drawAreaRect = new Rect(10f, _sceneWindowHeight - AreaHeight - 6f, AreaWidth, AreaHeight);
            GUILayout.BeginArea(drawAreaRect);
            GUILayout.BeginHorizontal();
           
            if (GUILayout.Button("<", GUILayout.Width(25))) {
                SelectPreviousSimulatorDevice();
            }

            ForActiveDeviceSimulatorMain(deviceSimulatorMain => {
                foreach (var deviceInHotBar in _deviceInHotBars) {
                    if (deviceInHotBar.Index == SimulatorDeviceInHotBar.InvalidIndex) {
                        continue;
                    }
                    var isSelected = (int)DeviceSimulatorMainDeviceIndexField.Value.GetValue(deviceSimulatorMain) == deviceInHotBar.Index;
               
                    if (GUILayout.Button(deviceInHotBar.DisplayInHotBarName, isSelected ? SelectedButtonStyle.Value : GUI.skin.button, GUILayout.Width(50))) {
                        SelectSimulatorDevice(deviceInHotBar.FriendlyName);
                    }
                }
            });

            if (GUILayout.Button(">", GUILayout.Width(25))) {
                SelectNextSimulatorDevice();
            }

            GUILayout.EndHorizontal();
            GUILayout.EndArea();
            Handles.EndGUI();

            var currentEvent = Event.current;
            if (currentEvent.type == EventType.ScrollWheel && drawAreaRect.Expand(15f).Contains(currentEvent.mousePosition)) {
                if (currentEvent.delta.y < 0) {
                    SelectPreviousSimulatorDevice();
                }
                else {
                    SelectNextSimulatorDevice();
                }
               
                currentEvent.Use();
            }
        }

        private static void WarmUpSimulatorDeviceIndexes() {
            if (_isSimulatorDeviceIndexesWarmedUp) {
                return;
            }

            ForActiveDeviceSimulatorMain(deviceSimulatorMain => {
                var devices = DeviceSimulatorMainDevicesField.Value.GetValue(deviceSimulatorMain) as Array;
               
                var index = 0;
                foreach (var device in devices) {
                    var deviceInfo = DeviceInfoAssetDeviceInfoField.Value.GetValue(device);
                    var friendlyName = DeviceInfoFriendlyNameField.Value.GetValue(deviceInfo).ToString();
                    foreach (var deviceInHotBar in _deviceInHotBars) {
                        if (deviceInHotBar.FriendlyName == friendlyName) {
                            deviceInHotBar.Index = index;
                        }
                    }

                    ++index;
                }
               
                _isSimulatorDeviceIndexesWarmedUp = true;
            });
        }

        private static void SelectNextSimulatorDevice() {
            ForActiveDeviceSimulatorMain(deviceSimulatorMain => {
                var currentIndex = DeviceSimulatorMainDeviceIndexField.Value.GetValue(deviceSimulatorMain);
                var nextIndex = (int)currentIndex + 1;
                var devices = DeviceSimulatorMainDevicesField.Value.GetValue(deviceSimulatorMain) as Array;
               
                if (nextIndex >= devices.Length) {
                    nextIndex = 0;
                }

                SelectSimulatorDevice(nextIndex);
            });
        }

        private static void SelectPreviousSimulatorDevice() {
            ForActiveDeviceSimulatorMain(deviceSimulatorMain => {
                var currentIndex = DeviceSimulatorMainDeviceIndexField.Value.GetValue(deviceSimulatorMain);
                var prevIndex = (int)currentIndex - 1;
               
                if (prevIndex < 0) {
                    var devices = DeviceSimulatorMainDevicesField.Value.GetValue(deviceSimulatorMain) as Array;
                    prevIndex = devices.Length - 1;
                }

                SelectSimulatorDevice(prevIndex);
            });
        }

        private static void SelectSimulatorDevice(int index) {
            ForActiveDeviceSimulatorMain(deviceSimulatorMain => {
                DeviceSimulatorMainDeviceIndexProperty.Value.SetValue(deviceSimulatorMain, index);
            });
        }

        private static void SelectSimulatorDevice(string deviceName) {
            ForActiveDeviceSimulatorMain(deviceSimulatorMain => {
                var devices = DeviceSimulatorMainDevicesField.Value.GetValue(deviceSimulatorMain) as Array;

                var index = 0;
                foreach (var device in devices) {
                    var deviceInfo = DeviceInfoAssetDeviceInfoField.Value.GetValue(device);
                    var friendlyName = DeviceInfoFriendlyNameField.Value.GetValue(deviceInfo).ToString();
                    if (friendlyName == deviceName) {
                        DeviceSimulatorMainDeviceIndexProperty.Value.SetValue(deviceSimulatorMain, index);
                        return;
                    }

                    ++index;
                }
            });
        }

        private static void ForActiveDeviceSimulatorMain(Action<object> callback) {
            var simulatorWindow = Resources.FindObjectsOfTypeAll(SimulatorWindowType.Value).FirstOrDefault();
            if (simulatorWindow == null) {
                return;
            }

            var deviceSimulatorMain = SimulatorWindowMainField.Value.GetValue(simulatorWindow);
            if (deviceSimulatorMain != null) {
                callback(deviceSimulatorMain);
            }
        }

        private static bool IsInSimulatorState() {
            if (ActiveGameView == null) {
                return false;
            }

            return ActiveGameView.titleContent.text == "Simulator";
        }

        private class SimulatorDeviceInHotBar {
            public const int InvalidIndex = -1;
           
            public string FriendlyName;
            public string DisplayInHotBarName;

            public int Index = InvalidIndex;
        }
    }
}
#endif
4 Likes

Unity actually supports adding “plugins” to the Device Simulator that show when you click on the Control Panel button.

I adapted TaoBao’s code as such.
Note: I simplified it to my needs (It no longer needs to be Lazy, and I removed the shortcut device names).

using System;
using System.Reflection;
using UnityEditor.DeviceSimulation;
using UnityEngine;
using UnityEngine.UIElements;

namespace ACME.Tools.DeviceSimulatorHelper
{
    public sealed class DeviceSwitcher : DeviceSimulatorPlugin
    {
        readonly FieldInfo _mainField;
        readonly PropertyInfo _deviceIndexProperty;
        readonly FieldInfo _devicesField;
        readonly UnityEngine.Object _simulatorWindow;

        public override string title => "Device Switcher";

        public DeviceSwitcher()
        {
            var assembly = Assembly.GetAssembly(typeof(DeviceSimulator));
            var windowType = assembly.GetType("UnityEditor.DeviceSimulation.SimulatorWindow");
            _mainField = windowType.GetField("m_Main", BindingFlags.Instance | BindingFlags.NonPublic);
            var mainType = assembly.GetType("UnityEditor.DeviceSimulation.DeviceSimulatorMain");
            _deviceIndexProperty = mainType.GetProperty("deviceIndex", BindingFlags.Instance | BindingFlags.Public);
            _devicesField = mainType.GetField("m_Devices", BindingFlags.Instance | BindingFlags.NonPublic);

            _simulatorWindow = Resources.FindObjectsOfTypeAll(windowType)[0];
        }

        public override VisualElement OnCreateUI()
        {
            var root = new VisualElement();
            root.style.flexDirection = FlexDirection.Row;

            var previousButton = new RepeatButton(SelectPreviousSimulatorDevice, 500, 250);
            var nextButton = new RepeatButton(SelectNextSimulatorDevice, 500, 250);
            previousButton.text = "Previous";
            nextButton.text = "Next";
            previousButton.AddToClassList("unity-button");
            nextButton.AddToClassList("unity-button");

            root.Add(previousButton);
            root.Add(nextButton);

            return root;
        }

        private void SelectPreviousSimulatorDevice() => SelectDeviceWithOffset(-1);

        private void SelectNextSimulatorDevice() => SelectDeviceWithOffset(1);

        private void SelectDeviceWithOffset(int offset)
        {
            var main = _mainField.GetValue(_simulatorWindow);
            var devices = _devicesField.GetValue(main) as Array;

            var index = (int)_deviceIndexProperty.GetValue(main);
            index = (int)Mathf.Repeat(index + offset, devices!.Length);

            _deviceIndexProperty.SetValue(main, index);
        }
    }
}
2 Likes