tafuji's blog

C#, Xamarin, Azure DevOps を中心に書いています。

Xamarin.Essentials 入門 - #6 デバイス情報

はじめに

この記事は、Xamarin Advent Calendar 2020 の 23日目の記事です。

今回の記事は、Xamarin.Essentials のデバイス情報を取得する API について記載します。この API を利用することで、アプリケーションを実行しているデバイスのプラットフォーム(iOSAndroid など)、デバイスの種類(実機、あるいはシミュレーターなどの仮想デバイス)などの情報を取得することができます。

使ってみよう

使い方

API で取得できるデバイス情報は、以下の通りです。

# プロパティ名 説明
1 Model モデル名を取得します
2 Manufacturer バイスの製造者名を取得します
3 VersionString バイスの OS バージョンを表す文字列を取得します
4 Version バイスの OS バージョンを System.Version で取得します
5 Platform バイスのプラットフォーム(iOS, Android, UWP など)を取得します
6 Idiom バイスのイディオム(電話、タブレット、腕時計など)を取得します
7 DeviceType バイスの種類を取得します

プラットフォームは、Xamarin.Essentials で定義される DevicePlatform 構造体の値(iOS, Android, UWP, macOS など)が返されます。

バイスのイディオムは、DeviceIdiom 構造体の値が返されます。

  • DeviceIdiom.Phone:電話
  • DeviceIdiom.Tabletタブレット
  • DeviceIdiom.Desktop:デスクトップ
  • DeviceIdiom.TV:テレビ
  • DeviceIdiom.Watch:腕時計
  • DeviceIdiom.Unknown:不明

また、デバイスの種類は、DeviceType 列挙体で定義された値が返されます。

使い方は非常にシンプルで、DeviceInfo クラスの static なプロパティを参照することで、デバイス情報を取得することができます。

// モデル名
var device = DeviceInfo.Model;

// 製造者名
var manufacturer = DeviceInfo.Manufacturer;

// デバイス名
var deviceName = DeviceInfo.Name;

// OS のバージョン
var version = DeviceInfo.VersionString;

// プラットフォーム
var platform = DeviceInfo.Platform;

// イディオム(電話、タブレットなど)
var idiom = DeviceInfo.Idiom;

// デバイスの種類(実機、仮想)
var deviceType = DeviceInfo.DeviceType;

読んでみよう

デバイス情報 のコードを読んでプラットフォーム固有の処理を見ていきます。

iOS

iOS プラットフォーム固有の処理が実装された DeviceInfo.ios.tvos.watchos.cs の処理を見ていきます。

モデル名

システム情報を取得する sysctlbyname 関数を利用します。sysctlbyname 関数をP/Invokeで実行してモデル名を取得します。 ただし、この方法で取得したモデル名は、iPhone10,6 のような値なので、iPhone X などの機種名を取得したい場合は、取得した値からモデル名にマッピングする処理を自分で書く必要があります。

値とモデル名の関係は、"GitHub Gist :Apple_mobile_device_types.txt" などにまとめられているので、これを参考にするとよいでしょう。ただし、新しい機種が発表されるたびにモデル名を調べるてマッピングを追加する必要があります。

#if __IOS__
[DllImport(Constants.SystemLibrary, EntryPoint = "sysctlbyname")]
#else
[DllImport(Constants.libSystemLibrary, EntryPoint = "sysctlbyname")]
#endif
internal static extern int SysctlByName([MarshalAs(UnmanagedType.LPStr)] string property, IntPtr output, IntPtr oldLen, IntPtr newp, uint newlen);

internal static string GetSystemLibraryProperty(string property)
{
    var lengthPtr = Marshal.AllocHGlobal(sizeof(int));
    SysctlByName(property, IntPtr.Zero, lengthPtr, IntPtr.Zero, 0);

    var propertyLength = Marshal.ReadInt32(lengthPtr);

    if (propertyLength == 0)
    {
        Marshal.FreeHGlobal(lengthPtr);
        throw new InvalidOperationException("Unable to read length of property.");
    }

    var valuePtr = Marshal.AllocHGlobal(propertyLength);
    SysctlByName(property, valuePtr, lengthPtr, IntPtr.Zero, 0);

    var returnValue = Marshal.PtrToStringAnsi(valuePtr);

    Marshal.FreeHGlobal(lengthPtr);
    Marshal.FreeHGlobal(valuePtr);

    return returnValue;
}

製造者名

固定の "Apple" という文字列を返しています。 以下、該当部分のコードの抜粋です。

static string GetManufacturer() => "Apple";

バイス

バイス名は、UIDevice クラスの Name プロパティから取得しています。 以下、該当部分のコードの抜粋です。

static string GetDeviceName() => UIDevice.CurrentDevice.Name;

OS のバージョン

OS のバージョンは、UIDevice クラスの SystemVersion プロパティから取得しています。 以下、該当部分のコードの抜粋です。

static string GetVersionString() => UIDevice.CurrentDevice.SystemVersion

プラットフォーム

プラットフォームは、条件付きコンパイル命令を使うことで、コンパイル時にそれぞれのプラットフォームに該当する値を返すように実装されています。以下、該当部分のコードの抜粋です。

        static DevicePlatform GetPlatform() =>
#if __IOS__
            DevicePlatform.iOS;
#elif __TVOS__
            DevicePlatform.tvOS;
#elif __WATCHOS__
            DevicePlatform.watchOS;
#endif

イディオム

バイスのイディオムは、UIDevice クラスの UserInterfaceIdiom プロパティを利用してデバイスの種類を判定しています。ただし、UserInterfaceIdiom プロパティで返される UIUserInterfaceIdiom 列挙体には、Apple Watch の場合の値が定義されていません。そのため、Apple Watch の場合は、条件付きコンパイル命令 #if __WATCHOS__ を使って、Apple Watch 向けにコンパイルされる場合は DeviceIdiom.Watch を返すように実装されています。 以下、該当部分のコードの抜粋です(条件付きコンパイル命令の部分は省略)。

switch (UIDevice.CurrentDevice.UserInterfaceIdiom)
{
    case UIUserInterfaceIdiom.Pad:
        return DeviceIdiom.Tablet;
    case UIUserInterfaceIdiom.Phone:
        return DeviceIdiom.Phone;
    case UIUserInterfaceIdiom.TV:
        return DeviceIdiom.TV;
    case UIUserInterfaceIdiom.CarPlay:
    case UIUserInterfaceIdiom.Unspecified:
    default:
        return DeviceIdiom.Unknown;
}

バイスの種類

バイスの種類は、Runtime クラスの Arch フィールドを利用してデバイスの種類を判定しています。 以下、該当部分のコードの抜粋です。

static DeviceType GetDeviceType()
    => Runtime.Arch == Arch.DEVICE ? DeviceType.Physical : DeviceType.Virtual;

Android

Android プラットフォーム固有の処理が実装された DeviceInfo.android.cs の処理を見ていきます。

モデル名

モデル名は、Build クラスの Model プロパティの値を返しています。 該当部分のコードからの抜粋を記載します。

static string GetModel() => Build.Model;

製造者名

製造者名は、Build クラスの Manufacturer プロパティの値を返しています。 該当部分のコードからの抜粋を記載します。

static string GetManufacturer() => Build.Manufacturer;

バイス

バイス名は、Settings.Global クラスの GetString メソッドに "device_name" を与えて、デバイス名を取得しています。コードを追いかけていくと、デバイス名を取得するメソッドの中から、システム情報を取得するメソッドを呼び出していることが分かります。以下は、ポイントとなる部分の抜粋です。

// static string GetDeviceName() メソッドの中で、システム情報を取得するメソッドを呼び出す。
var name = GetSystemSetting("device_name", true);

// GetSystemSetting("device_name", true) を呼び出したときの処理
if (isGlobal && Essentials.Platform.HasApiLevelNMr1)
    return Settings.Global.GetString(Essentials.Platform.AppContext.ContentResolver, name);

OS のバージョン

OS のバージョンは、Build.VERSION クラスの Release プロパティの値を返しています。 該当部分を抜粋します。

static string GetVersionString() => Build.VERSION.Release;

プラットフォーム

プラットフォームは、DevicePlatform.Android を返しています。

static DevicePlatform GetPlatform() => DevicePlatform.Android;

イディオム

イディオムの判定は、まず UiModeManager クラスの CurrentModeType プロパティの値から、判定を行いますが、電話かタブレットかの判定は、スクリーンサイズから判定しています。以下は、コードの抜粋です。

// UiMode の値からイディオムを試みる
var uiMode = uiModeManager?.CurrentModeType ?? UiMode.TypeUndefined;
currentIdiom = DetectIdiom(uiMode);


// UiMode によるイディオムの判定処理。
// テレビ、デスクトップ、腕時計以外は、ここでは判定できない
static DeviceIdiom DetectIdiom(UiMode uiMode)
{
    if (uiMode == UiMode.TypeNormal)
        return DeviceIdiom.Unknown;
    else if (uiMode == UiMode.TypeTelevision)
        return DeviceIdiom.TV;
    else if (uiMode == UiMode.TypeDesk)
        return DeviceIdiom.Desktop;
    else if (Essentials.Platform.HasApiLevel(BuildVersionCodes.KitkatWatch) && uiMode == UiMode.TypeWatch)
        return DeviceIdiom.Watch;

    return DeviceIdiom.Unknown;
}

// スマートフォンかタブレットかの判定は、以下のような処理で行っている
var configuration = Essentials.Platform.AppContext.Resources?.Configuration;
if (configuration != null)
{
    var minWidth = configuration.SmallestScreenWidthDp;
    var isWide = minWidth >= tabletCrossover;
    currentIdiom = isWide ? DeviceIdiom.Tablet : DeviceIdiom.Phone;
}
else
{
    // start clutching at straws
    using var metrics = Essentials.Platform.AppContext.Resources?.DisplayMetrics;
    if (metrics != null)
    {
        var minSize = Math.Min(metrics.WidthPixels, metrics.HeightPixels);
        var isWide = minSize * metrics.Density >= tabletCrossover;
        currentIdiom = isWide ? DeviceIdiom.Tablet : DeviceIdiom.Phone;
    }
}

バイスの種類

エミュレーターかどうかの判定が、かなり複雑です。Build クラスのいくつかのプロパティを取得して、その値に含まれる文字列で判定しています。 該当部分を抜粋しておきます。

var isEmulator =
    (Build.Brand.StartsWith("generic", StringComparison.InvariantCulture) && Build.Device.StartsWith("generic", StringComparison.InvariantCulture)) ||
    Build.Fingerprint.StartsWith("generic", StringComparison.InvariantCulture) ||
    Build.Fingerprint.StartsWith("unknown", StringComparison.InvariantCulture) ||
    Build.Hardware.Contains("goldfish") ||
    Build.Hardware.Contains("ranchu") ||
    Build.Model.Contains("google_sdk") ||
    Build.Model.Contains("Emulator") ||
    Build.Model.Contains("Android SDK built for x86") ||
    Build.Manufacturer.Contains("Genymotion") ||
    Build.Manufacturer.Contains("VS Emulator") ||
    Build.Product.Contains("emulator") ||
    Build.Product.Contains("google_sdk") ||
    Build.Product.Contains("sdk") ||
    Build.Product.Contains("sdk_google") ||
    Build.Product.Contains("sdk_x86") ||
    Build.Product.Contains("simulator") ||
    Build.Product.Contains("vbox86p");

if (isEmulator)
    return DeviceType.Virtual;

UWP

UWP プラットフォーム固有の処理が実装された DeviceInfo.uwp.cs の処理を見ていきます。

モデル名

モデル名は、EasClientDeviceInformation クラスの SystemProductName プロパティの値を返しています。

// EasClientDeviceInformation クラスをフィールドで定義
static readonly EasClientDeviceInformation deviceInfo;

// 静的コンストラクタで EasClientDeviceInformation のインスタンスを生成
static DeviceInfo()
{
    deviceInfo = new EasClientDeviceInformation();
    // 以下の処理は省略
}

// モデル名の取得
static string GetModel() => deviceInfo.SystemProductName;

製造者名

製造者名は、EasClientDeviceInformation クラスの SystemManufacturer プロパティの値を返しています。

static string GetManufacturer() => deviceInfo.SystemManufacturer;

バイス

バイス名は、EasClientDeviceInformation クラスの FriendlyName プロパティの値を返しています。

static string GetDeviceName() => deviceInfo.FriendlyName;

OS のバージョン

バージョンは、AnalyticsInfo クラスの VersionInfo プロパティ経由で、AnalyticsVersionInfo クラスのDeviceFamilyVersion の値を取得し、メジャー、マイナー、ビルド、リビジョン番号を取得しています。 番号を取得するロジックは、StackOverflow の "Windows 10 get DeviceFamilyVersion" に同じようなコードのサンプルがありました。

static string GetVersionString()
{
    var version = AnalyticsInfo.VersionInfo.DeviceFamilyVersion;

    if (ulong.TryParse(version, out var v))
    {
        var v1 = (v & 0xFFFF000000000000L) >> 48;
        var v2 = (v & 0x0000FFFF00000000L) >> 32;
        var v3 = (v & 0x00000000FFFF0000L) >> 16;
        var v4 = v & 0x000000000000FFFFL;
        return $"{v1}.{v2}.{v3}.{v4}";
    }

    return version;
}

プラットフォーム

プラットフォームは、UWP なので、DevicePlatform.UWP の値を返しています。

static DevicePlatform GetPlatform() => DevicePlatform.UWP;

イディオム

イディオムは、AnalyticsVersionInfo クラスの DeviceFamily の値に含まれる文字列で判定しています。 以下、判定処理の部分を抜粋しています。

switch (AnalyticsInfo.VersionInfo.DeviceFamily)
{
    case "Windows.Mobile":
        currentIdiom = DeviceIdiom.Phone;
        break;
    case "Windows.Universal":
    case "Windows.Desktop":
        {
            try
            {
                var uiMode = UIViewSettings.GetForCurrentView().UserInteractionMode;
                currentIdiom = uiMode == UserInteractionMode.Mouse ? DeviceIdiom.Desktop : DeviceIdiom.Tablet;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Unable to get device . {ex.Message}");
            }
        }
        break;
    case "Windows.Xbox":
    case "Windows.Team":
        currentIdiom = DeviceIdiom.TV;
        break;
    case "Windows.IoT":
    default:
        currentIdiom = DeviceIdiom.Unknown;
        break;
}

バイスの種類

バイスの種類は、EasClientDeviceInformation クラスの SystemProductName プロパティに含まれる文字列から判定をしています。

systemProductName = deviceInfo.SystemProductName;

var isVirtual = systemProductName.Contains("Virtual") || systemProductName == "HMV domU";

おわりに

今年も、Xamarin Advent Calendar に参加させてもらい、とても嬉しく思います。 来年も書けるといいなぁと思っています。

参考