設定を外部定義にしたい
HoloLensやWinMR問わずアプリケーションの環境設定(例えばサーバの接続先等の環境に影響するもの)を別ファイルにして管理しておき必要に応じて書き換えるというのはよくやる方法だと思います。
そういうシチュエーションをHoloLensでする必要があったのでいろいろ方法を模索しました。
環境設定ファイルの置き場所
環境設定ファイルを実行時に読ませる場合、置き場所が問題となります。特にHoloLensの場合システム上ファイルを保管しておける場所が限られます。
いくつか候補を検討した結果以下の場所がよさそうです。
種類 | 説明 | パス |
---|---|---|
LocalFolder | アプリケーション内で利用するファイルの格納するフォルダ | C:\Users[ユーザ名]\AppData\Local\Packages[パッケージファミリ名]\LocalState |
アプリケーションのルート | UWPではAppxに含まれるファイルを指定して読み込む機能があります。これを利用して環境設定ファイルを作成します。環境設定ファイルを書き換える場合はAppxの再セットアップが必要。 | ms-appx:///[ファイル名] |
上記を選定している理由は、特別な権限がなくてもファイルアクセスが可能となっているためです。アプリケーションを不用意に権限拡大することはあまりセキュアでもないですし。
アプリケーションのルートはインストール前にAppxの書き換えが必要ではあるのですがHoloLensに限定するとこの方法くらいしかないと思います(あれば教えてほしいです)
HoloLensが難しい理由
HoloLensはWin10でUWPで動いてはいるのですが、デバイス上のすべてのパスを見ることができません。USBで接続してもメディアに関する情報が中心に表示されるのみです。またDevice Portalから確認するとアプリケーション毎のフォルダを確認はできます。ただし、これらのフォルダにはUploadがうまくできない状態になっています。OSをRS4のInsider版に変えるとDevice Portalからアップロードもできるのですが、フォルダが空の場合はアップロードできません。
つまり、通常の方法に比べても環境設定ファイルの配置はなかなか厄介な問題になります。
1つの回避方法として以下の形を試しました。
環境設定ファイルについてはAppxに含める形で構成します。アプリケーション開始時にこの外部定義を読込むんですが、処理フローは以下のようにしてみました。読込先を①LocalFolder、②アプリケーションのルートの順でファイルを探して読込みします。①に関する処理を追加したのは、HoloLensのRS4 InsiderPreview版であればLocalFolderにファイルを置くことで、Device Portal上で上書きできるようになるためです(この仕様になってくれるといいんですけどねぇ)。
開発環境とサンプルコード
今回の環境は以下の通りです。HoloLens はRS4 InsiderPreview版ですがサンプルコードは通常版でも問題ありません。
- Windows 10
- Visual Studio 2017 Community Edition
- Unity 2017.2.1p2
- Mixed Reality Toolkit 2017.2.1.4(Singletonコンポーネントのみ利用)
- HoloLensはRS4
サンプルコード
設定ファイルを処理するためのApplicationProfileをsingletonで作成しています。
読込みと書込みは手動にしています。LocalFolderにファイルがない場合はアプリケーションルートのファイルを読み込みます。それ以降はLocalFolderにファイルI/Oを行う仕様です。
// Copyright(c) 2018 Takahiro Miyaura
// Released under the MIT license
// http://opensource.org/licenses/mit-license.php
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using HoloToolkit.Unity;
using UnityEngine;
#if WINDOWS_UWP
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
#else
using System.Text;
#endif
public class ApplicationProfile : Singleton<ApplicationProfile>
{
private readonly Dictionary<string, string> _applicationProfile = new Dictionary<string, string>();
public string OutputFileName;
public string this[string key]
{
get { return _applicationProfile[key]; }
}
public void Start()
{
}
public bool RemoveParameter(string key)
{
var result = false;
if (_applicationProfile.ContainsKey(key))
{
_applicationProfile.Remove(key);
result = true;
}
return result;
}
public bool SetParameter(string key, string value, bool valueOverride = false)
{
var result = false;
if (_applicationProfile.ContainsKey(key) && valueOverride)
{
_applicationProfile[key] = value;
result = true;
}
else if (!_applicationProfile.ContainsKey(key))
{
_applicationProfile[key] = value;
result = true;
}
return result;
}
public void WriteFile()
{
var jsonApplicationProfile = new JsonApplicationProfile();
jsonApplicationProfile.Parameters =
_applicationProfile.Select(x => new Parameter {key = x.Key, value = x.Value}).ToArray();
var jsonData = JsonApplicationProfile.ToJson(jsonApplicationProfile);
#if WINDOWS_UWP
var localFolder = ApplicationData.Current.LocalFolder;
var file =
localFolder.CreateFileAsync(OutputFileName, CreationCollisionOption.ReplaceExisting).GetAwaiter().GetResult();
FileIO.WriteTextAsync(file, jsonData, UnicodeEncoding.Utf8).GetAwaiter().GetResult();
#else
var fi = new FileInfo(Application.dataPath + "/" + OutputFileName);
using (var sw = fi.CreateText())
{
sw.WriteLine(jsonData);
}
#endif
}
public bool ReadFile()
{
var isTemp = false;
var result = true;
var jsonData = string.Empty;
#if WINDOWS_UWP
var localFolder = ApplicationData.Current.LocalFolder;
var item = localFolder.TryGetItemAsync(OutputFileName).GetAwaiter().GetResult();
StorageFile file = null;
if (item == null)
{
var uri = new Uri("ms-appx:///"+ OutputFileName);
var storageFile = Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(uri).GetAwaiter().GetResult();
if (storageFile != null)
{
file = storageFile;
isTemp = true;
}
}
else
{
file = localFolder.GetFileAsync(OutputFileName).GetAwaiter().GetResult();
}
if (file == null) return false;
jsonData = FileIO.ReadTextAsync(file, UnicodeEncoding.Utf8).GetAwaiter().GetResult();
#else
var fi = new FileInfo(Application.dataPath + "/" + OutputFileName);
try
{
using (var sr = new StreamReader(fi.OpenRead(), Encoding.UTF8))
{
jsonData = sr.ReadToEnd();
}
}
catch (Exception e)
{
Debug.Log(e.ToString());
}
#endif
try
{
var jsonApplicationProfile = JsonApplicationProfile.FromJson(jsonData);
foreach (var parameter in jsonApplicationProfile.Parameters)
_applicationProfile.Add(parameter.key, parameter.value);
}
catch (Exception e)
{
result = false;
}
#if WINDOWS_UWP
if (isTemp)
{
WriteFile();
}
#endif
return result;
}
[Serializable]
public class JsonApplicationProfile
{
public Parameter[] Parameters;
public static JsonApplicationProfile FromJson(string json)
{
return JsonUtility.FromJson<JsonApplicationProfile>(json);
}
public static string ToJson(JsonApplicationProfile profile)
{
return JsonUtility.ToJson(profile);
}
}
[Serializable]
public class Parameter
{
public string key;
public string value;
}
}
ファイルはJSON形式でKey-Valueの配列として定義します。
{"Parameters":
[
{"key":"SpeechTranslateUrl","value":"wss://dev.microsofttranslator.com/speech/translate?"},
{"key":"LanguageUrl","value":"https://dev.microsofttranslator.com/languages?"},
{"key":"TokenUrl","value":"https://api.cognitive.microsoft.com/sts/v1.0/issueToken?"},
{"key":"SubScriptionKey","value":""},
{"key":"ApiVersion","value":"1.0"},
{"key":"ProxyAddress","value":""},
{"key":"ProxyUserName","value":""},
{"key":"ProxyPassword","value":""}
]
}
使い方
ApplicationProfileは空のGameObjectを作成し、Hierarchyに登録しておきます。
ApplicationProfileはOutputFileNameを設定値に持っています。ここに任意のファイル名を指定します。
環境設定ファイルからの情報を取得する場合はキー値で取得します。
var profile = ApplicationProfile.Instance;
profile.ReadFile();
service.SpeechTranslateUrl = profile["SpeechTranslateUrl"];
設定ファイルの要素は追加/削除可能です。
var profile = ApplicationProfile.Instance;
//パラメータを追加する。第3引数がtrueの場合は値の上書き
profile.SetParameter("SpeechTranslateUrl","Foo",true);
//パラメータを削除する。
profile.RemoveParameter("SpeechTranslateUrl");
//変更内容を保存する
profile.WriteFile();
最後に環境設定ファイルをアプリケーションルートの格納する準備をします。図の例はApplicationProfile.configというファイル名で環境設定ファイルを扱う場合のものです。
Unityからビルドした後、Visual Studioを開きます。
UWPプロジェクトの直下に環境設定ファイルを追加します。追加後外部設定ファイルのプロパティの[ビルドアクション]を[コンテンツ]に変更します。
この操作によって環境設定ファイルがAppx内に格納されます。
後は、普通にアプリケーションを起動すれば初回のみアプリケーションルートの情報を読込み、2回目以降はLocalFolderから読込みを行います。実行後に
Device PortalからFile ExplorerでLocalFolderを確認するとファイルが作成されていると思います。RS4であればこの状態でファイルのアップロードも可能になり上書き可能です。
最後に
変更が入る可能性のある可変値をファイルで外部定義する方法の1つの手段として試してみました。正直もう少し分かりやすくていい方法があるとは思うのでもっとやりやすい方法に気づいた方は是非教えてください。