ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

ストアアプリのターゲットを Windows 8 から 8.1 に変更する際は Flexbox レイアウトの CSS の変更に注意

前置き : ストアアプリにおける可変ボックス (Flexbox) レイアウト

Windows ストアアプリを HTML + JS で構築する際のレイアウトの選択肢は以下のページにいろいろと挙げられています。

そのうちの一つに可変ボックスレイアウトがあります。 CSS Flexible Box Layout Module を使うというものです。

可変ボックスレイアウトの Windows 8 プラットフォームと 8.1 プラットフォームでの違い

Windows 8 プラットフォーム向けのストアアプリでは、2012 年 3 月 22 日版の 「Flexbox」 草案に基づいた実装になっているようです。

仕様が古く、また、-ms- ベンダプレフィクスが必要 *1 です。

Windows 8.1 プラットフォーム向けのストアアプリでは、2012 年 9 月の勧告候補に基づいた実装になっているようです。 おそらくですが。

さて、Windows 8 プラットフォーム向けに記述したベンダプレフィクス付きの CSS プロパティや値は、Windows 8.1 プラットフォームでは対応するベンダプレフィクス無しのものとして解釈されるようです。 例えば以下のようになりました。

  • display: -ms-flexbox;display: flex; として扱われ、
  • -ms-flex: 0 0 auto;flex: 0 0 auto; として扱われる。

よって、基本的には Windows 8 プラットフォーム向けに書いた CSS はそのまま Windows 8.1 プラットフォームでも同様に解釈されるのですが、一部仕様が変わっている部分があるので注意が必要です。

flex (-ms-flex) プロパティの初期値の変更

例えば、Windows 8 プラットフォームにおいて -ms-flex プロパティの初期値は none (0 0 auto と同等) でした。 一方、Windows 8.1 プラットフォームにおいて flex プロパティの初期値は 0 1 auto に変更されました。

そのため、次のような HTML を書いていた場合、ターゲットが Windows 8Windows 8.1 かで表示がかわってしまいます。

<div style="display: -ms-flexbox; -ms-flex-direction: column; height: 300px; width: 300px">
    <div><!-- ← Windows 8 プラットフォームでは `-ms-flex: none;` 相当。 8.1 では `flex: 0 1 auto;` 相当。 -->
        <div style="height: 200px; background-color: rgba(200, 200, 0, 0.5);">1</div>
    </div>
    <div style="-ms-flex: 1 1 auto;">
        <div style="height: 200px; background-color: rgba(0, 200, 200, 0.5);">2</div>
    </div>
</div>

実際にそれぞれのプラットフォームでどのような表示になるかは次の図をご覧ください。 (ボーダーなど一部の CSS は別に指定しています。)

f:id:nobuoka:20150506172113p:plain

初期値の変更ということで、明示的に -ms-flex プロパティの値を指定していた場合は影響がないですが、省略していた場合はターゲットを Windows 8 から 8.1 に変更した際に影響があるかもしれません。

他の CSS プロパティについて Windows 8 と 8.1 で変更があったものがあるかどうかは調べていませんが、他にもあるかもしれません。

ストアアプリのターゲットを Windows 8 から 8.1 に変更する際の参考文献

ストアアプリのターゲットを Windows 8 から 8.1 に変更する際に下記ページ群が参考になります。 ただし、今回の可変ボックスレイアウト周りの変更については触れられていなかったりするので気を付けましょう!

*1:Windows 8.1 上で Windows 8 プラットフォーム向けにビルドしたストアアプリを動かしたところベンダプレフィクスがなくても動きましたが、多分 Windows 8 だと必要な気がします。

WebAuthenticationBroker を使用して OAuth による認可処理を Windows ストアアプリ内に組み込む

Windows ストアアプリ開発の話です。 Windows 8.1 および Windows Phone 8.1 を対象とした内容です。 (8.0 以前あるいは 10 以降については触れません。)

Authentication and User Identity (HTML)」 に書かれているように、Windows ストアアプリ内にユーザー認証の機能を組み込むための選択肢はいろいろあります。 このエントリでは、その中の一つである Web 認証ブローカー (Web authentication broker) について紹介します。

Web 認証ブローカー (Web authentication broker) の概要

Web 認証ブローカーは、OpenID や OAuth などのインターネット経由の認証プロトコル *1 を使うオンライン ID プロバイダーに接続してユーザー認証する (あるいは認可を得る) 際に利用できる機能です。

OAuth などのプロトコルでは、クライアントに許可を与えるためにユーザー (OAuth ではリソース保持者) がサーバー (ID プロバイダー) の web ページ上で操作する必要があります。 ユーザーにサーバーの web ページを表示するためにアプリ内に web ビューを表示して操作させる場合もありますが、自前で web ビューを用意する代わりに Web 認証ブローカーを使用することができます。

Web 認証ブローカーの仕組みなどは、次のページを見ると良いでしょう。

Web 認証ブローカーの使い方

OAuth 1.0 Protocol を用いて認可を得る場合を例に、どのように web 認証ブローカーを使用するかを見ていきます。

前準備

Web 認証ブローカーを使用する前に、予め Temporary Credentials を取得しておきます。 このとき、oauth_callback パラメータによりコールバック URI を指定できますが、任意の URI (「http://localhost/」 など) を指定して良いです。

サーバー側が対応しているのであれば WebAuthenticationBroker.getCurrentApplicationCallbackUri メソッド の返り値 (「ms-app://s-1-15-2-9999-999999999-999999-99999/」 みたいな URI) を使うのが良いです。 ただ、サーバー側が 「http(s)」 プロトコルしか許可していない場合もありますので、その場合は適当な http プロトコルURI を指定しましょう。

Web 認証ブローカーによる Resource Owner Authorization

Temporary Credentials を取得した後、リソース保持者による認可処理があります。 すなわち、ユーザーをサーバーに送り、リクエストを認可してもらう必要があります。 ここで web 認証ブローカーを使用できます。

Web 認証ブローカーには、最初にユーザーに表示する web ページの URI (OAuth では Resource Owner Authorization endpoint URI) と、認可された後のリダイレクト先 URI (Temporary Credentials 取得時に指定したコールバック URI) を指定します。

var Uri = Windows.Foundation.Uri;
var WebAuthenticationBroker = Windows.Security.Authentication.Web.WebAuthenticationBroker;
var WebAuthenticationOptions = Windows.Security.Authentication.Web.WebAuthenticationOptions;
var WebAuthenticationStatus = Windows.Security.Authentication.Web.WebAuthenticationStatus;

// はてなの OAuth の場合。 `tempCred.identifier` は先に取得しておいた temporary credentials の identifier。
var startUri = new Uri("https://www.hatena.ne.jp/oauth/authorize?oauth_token=" + encodeURIComponent(tempCred.identifier));
// Temporary credentials 取得時に `oauth_callback` で http://localhost/ を指定していた場合。
var endUri = new Uri("http://localhost/");

// WebAuthenticationBroker が authenticateAndContinue メソッドを持っているかどうかで処理を変える。
var authenticateAndContinue = WebAuthenticationBroker.authenticateAndContinue;
if (authenticateAndContinue) {
    // Windows Phone 8.1 用
    authenticateAndContinue(startUri, endUri, null, WebAuthenticationOptions.none);
        // 結果の受け取り方は下の記述を見ること。
} else {
    // Windows 8.1 用
    WebAuthenticationBroker.authenticateAsync(WebAuthenticationOptions.none, startUri, endUri).done(function (result) {
        if (result.responseStatus === WebAuthenticationStatus.success) {
            // `result.responseData` にはリダイレクト先 URI 文字列が入っている。
            // 例 : "http://localhost/?oauth_token=xxxxxxoauth_verifier=xxxxxx"
        } else {
            // 失敗 (ユーザー操作で戻って来た場合など。)
        }
    }, function (err) {
        // 失敗 (HTTP レベルでのエラーが発生した場合など。)
    });
}
Windows Phone 8.1 でのレスポンスの受け取り方

Windows 8.1 の場合は、WebAuthenticationBroker.authenticateAsync メソッドを呼ぶとアプリ内にサーバーのページが表示され、ユーザーによる認可が完了すると Promise から結果を受け取ることができます。

一方、Windows Phone 8.1 では、WebAuthenticationBroker.authenticateAndContinue メソッドを呼ぶと一旦アプリが停止してアプリの外部でサーバーのページが表示され、ユーザーによる認可が完了するとアプリがアクティベートされます。 メモリ使用量を抑えるために Windows Phone プラットフォームではこのような挙動になっているようです。

authenticateAndContinue メソッドのように xxxAndContinue という名前のメソッドがいくつかありますが、それらのメソッドを呼びだした後の結果はアクティベーションイベントとして受け取ることができます。 authenticateAndContinue メソッドではなくファイルピッカーの例ですが、次のページが参考になります。

XAML 版なので直接は利用できませんが、次のページの情報も参考になります。

ページナビゲーションをサポートするアプリの場合、次のような感じで WinJS.Applicationactivated イベントのリスナーを登録し、アクティベーションの情報を WinJS.Navigation.navigate メソッドに渡すようにします。

var SESSION_STAT_NAV_HISTORY = "nav_history";

var activation = Windows.ApplicationModel.Activation;
var app = WinJS.Application;
var nav = WinJS.Navigation;
var sched = WinJS.Utilities.Scheduler;
var ui = WinJS.UI;
var activationKinds = Windows.ApplicationModel.Activation.ActivationKind;

function activateOnLaunchOrContinuationProcs(args) {
    if (args.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
        // TODO: This application has been newly launched. Initialize your application here.
    } else {
        // TODO: This application has been reactivated from suspension. Restore application state here.
    }

    // Optimize the load of the application and while the splash screen is shown, execute high priority scheduled work.
    ui.disableAnimations();
    var p = ui.processAll().then(function () {
        var url = Application.navigator.home;
        var activationKind = args.kind;
        var activatedEventArgs = args;
        // アクティベーションの情報。 `xxxAndContinue` メソッドの結果もここに含まれる。
        var initialState = { activationKind: null, activatedEventArgs: null };
        var navHistory = app.sessionState[SESSION_STAT_NAV_HISTORY];
        if (navHistory) {
            url = navHistory.current.location;
            initialState = navHistory.current.state || initialState;
        }
        initialState.activationKind = activationKind;
        initialState.activatedEventArgs = activatedEventArgs;
        nav.history = navHistory || {};
        nav.history.current.initialPlaceholder = true;
        return nav.navigate(url, initialState);
    }).then(function () {
        return sched.requestDrain(sched.Priority.aboveNormal + 1);
    }).then(function () {
        ui.enableAnimations();
    });
    return p;
}

app.addEventListener("activated", (args) => {
    // Windows.ApplicationModel.Activation.IActivatedEventArgs インターフェイスを実装したオブジェクト。
    var activatedEventArgs = args.detail;
    switch (activatedEventArgs.kind) {
        case activationKinds.launch:
        case activationKinds.pickFileContinuation:
        case activationKinds.pickSaveFileContinuation:
        case activationKinds.pickFolderContinuation:
        case activationKinds.webAuthenticationBrokerContinuation:
            args.setPromise(activateOnLaunchOrContinuationProcs(activatedEventArgs));
            break;
        default:
            break;
    }
});

上のようにしてアクティベーションの情報を WinJS.Navigation.navigate メソッドに渡すようにすると、その情報がページコントロールにわたってくるので、ページコントロールの ready で次のように情報を受け取ることができます。

var ActivationKind = Windows.ApplicationModel.Activation.ActivationKind;
var WebAuthenticationStatus = Windows.Security.Authentication.Web.WebAuthenticationStatus;

WinJS.UI.Pages.define("/pages/auth/auth.html", {
    ready: function (element, options) {
        // Continuation handlers are specific to Windows Phone.
        if (options && options.activationKind === ActivationKind.webAuthenticationBrokerContinuation) {
            // Windows.ApplicationModel.Activation.IWebAuthenticationBrokerContinuationEventArgs インターフェイスを実装したオブジェクト。
            var eventArgs = options.activatedEventArgs;
            // eventArgs から WebAuthenticationBroker からの結果を取りだして処理する。
            var result = eventArgs.webAuthenticationResult;
            if (result.responseStatus === WebAuthenticationStatus.success) {
                // `result.responseData` にはリダイレクト先 URI 文字列が入っている。
                // 例 : "http://localhost/?oauth_token=xxxxxxoauth_verifier=xxxxxx"
            } else {
                // 失敗 (HTTP レベルでのエラーが発生した場合やユーザーがキャンセルした場合。)
                // ユーザーによるキャンセルと HTTP レベルでのエラーが発生した場合の区別ができなさそう?
                // (手元で試したところ HTTP レベルでのエラーでも `result.responseStatus` の値が 1 になっていた。)
            }
        }
    }
});

ちょっと面倒ですがしょうがないですね。

WebAuthenticationBroker から結果を受け取った後

ユーザーが認可を行った場合は WebAuthenticationBroker からの結果に oauth_verification が含まれているはずですので、それを使って Token Credentials の取得処理を行えば良いです。

サンプルプロジェクト

WebAuthenticationBroker を使用するサンプルプロジェクトがあります。 JS のものも他の言語のものもあるので参考になるでしょう。 ただ、OAuth 周りの処理は完全ではなさそう (OAuth のパーセントエンコードの代わりに encodeURIComponent が使われていたりする) なので、あくまで参考程度に。

セキュリティ的な話

アプリ内に web ビューを表示する方法だと、アプリが信頼できないものの場合にパスワードが盗まれる可能性があるという問題があるのでセキュリティ的には外部ブラウザを使う方法を提供するべき (下記ページ参照) なわけですが、web 認証ブローカーについても同様の問題がありそうな気がします。

Web 認証ブローカーは Windows 側が提供している API なので、「接続先 URL が表示される」 + 「Web 認証ブローカーを用いた場合と同様の表示をアプリ側が独自に実装できないようになっている」 という条件が満たされれば外部ブラウザを使用するのと同等になるのかなーと思いますが。 *2

そもそも Android アプリと違って審査があって、要件に 『4.3 アプリはユーザーのセキュリティ、Windows デバイス、システム、関連するシステムのセキュリティまたは機能を危険にさらしたり侵害したりしてはならない。また、Windows ユーザーまたは他の個人に被害を与える可能性があってはならない』 ってのがあるから、パスワードを抜こうとするようなアプリは審査で落とされるから大丈夫なのかもですけど。

*1:OAuth は認証のためのプロトコルではなく認可のためのプロトコルであるが、ここでは深く踏み込まない。

*2:あんまり考えずに書いてます。

Windows ストアアプリの JS で文字列をエンコードするために Windows Runtime コンポーネントを作成する

前置き: JS での Windows ストアアプリ開発時に C# の機能を使いたい

Windows ストアアプリを JS + HTML で開発する場合、たまに JS の非力さに困ることがあります。 例えば、文字列を UTF-8エンコードしてバイト列を得たいという場合。 JS だけでエンコードするのは難しいですよね。 JS での文字列のエンコードの現状としては、WHATWG の Encoding StandardTextEncoder インターフェイスが定義されていて、FirefoxChromeOpera では使えるようになっているようです。 しかし、残念ながら Windows ストアアプリでは使えません (Windows 8.1 時点)。

Windows ストアアプリで使える一つの方法として、エンコード処理を全て JS で記述するという方法があります。 JS の文字列を UTF-8エンコードして得られたバイト列を Uint8Array オブジェクトとして得る関数 (strToUTF8Arr 関数) のサンプルコードが MDN にあります。

このような関数を用意してもよいのですが、Windows ストアアプリ開発をする場合は C# の力を借りるということが簡単にできるので C# の力を借りるのが良いでしょう。 (C# だけでなく Visual BasicC++ も使用できますが、他の言語を選択する理由が特にないのであれば C# を使うのが楽だと思います。)

Windows Runtime コンポーネント (WinRT コンポーネント)

Windows ストアアプリの JS からは Windows Runtime API を触ることはできますが、ネイティブの C++ のライブラリを直接使用したり、.NET Framework のクラスライブラリを直接使用したりすることはできません。 JS から C++ .NET Framework の機能を使用するためには Windows Runtime を使ってコンポーネントを作成する必要があります。 Windows Runtime コンポーネントは、C#C++Visual Basic のいずれかの言語で作成できます。 作成された Windows Runtime コンポーネントは、JS からも使用できますし、当然他の言語 (C#C++Visual Basic) からも使用できます。

Windows Runtime コンポーネントの概要は次の記事が参考になります。

公式のドキュメントとしては次を見ると良いと思います。

MS によるブログエントリも参考になります。

実際の作り方

C#Windows Runtime コンポーネントを記述するには、次のチュートリアルを見るのが良いでしょう。

Visual Studio Professional (または Community) 2013 を使っていれば、上のチュートリアル通りに進めることで Windows Runtime コンポーネントを作成し、JS から使用するということができるはずです。

配列の扱いなどの注意すべき点は次の文書を読むと良いでしょう。 (日本語だとタイトルが 「C++ および〜」 となっていますが、正しくは 「C# および〜」 です。)

サンプル

例として、文字列を UTF-8 エンコーディングエンコードしたり、逆にデコードしたりするための Windows Runtime コンポーネントを作成して、JS から使用してみます。

まず、新たに C# 用の Windows Runtime Componenet プロジェクトテンプレートを使って、新しいプロジェクトを作成します。 今回は 「WindowsRuntimeComponentSample」 というプロジェクト名にしました。 そして、次のようなクラス (WindowsRuntimeComponentSample.Utf8Encoding クラス) を作成しました。

using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;

namespace WindowsRuntimeComponentSample
{
    public sealed class Utf8Encoding
    {
        public static byte[] encode(String str)
        {
            return Encoding.UTF8.GetBytes(str);
        }

        public static String decode([ReadOnlyArrayAttribute] byte[] bytes)
        {
            return decode(bytes, 0, bytes.Length);
        }

        public static String decode([ReadOnlyArrayAttribute] byte[] bytes, int index, int count)
        {
            return Encoding.UTF8.GetString(bytes, index, count);
        }
    }
}

そしてこのプロジェクトを JS のプロジェクト (プロジェクト名 「App1」) から参照するように設定します。 ソリューションの構成は次のようになりました。

f:id:nobuoka:20150329213852p:plain

これで JS から WindowsRuntimeComponentSample.Utf8Encoding クラスが使用できます。

// 文字列を UTF-8 でエンコードする。 (返り値は Uint8Array オブジェクト。)
WindowsRuntimeComponentSample.Utf8Encoding.encode("あ");
// バイト列 (Uint8Array オブジェクト) を UTF-8 でデコードする。
WindowsRuntimeComponentSample.Utf8Encoding.decode(new Uint8Array([0xE3, 0x81, 0x82]));

JS での string 型は C# では System.String 型になります。 また、C# での byte[] 型は JS では Uint8Array オブジェクトとして扱われます。

TypeScript で扱う際に型情報を自前で用意する必要があるのがちょっと面倒だなーと思いました。 *1

おわりに

UTF-8 での文字列エンコーディングを例にしましたが、文字列のエンコード・デコード処理以外でも JS では書きにくい処理を C# などで実装して JS から呼び出すということがさほど難しくなく実現できます。

Windows アプリ開発時にどの言語 (JS、C#C++Visual Basic) を選択するか考える際に 「基本的に JS で大丈夫だけど、JS で書きにくい処理を書く必要が出てきたらどうしよう」 と悩むこともあるかもしれません。 このエントリで見たように、そのような処理は C# などで Windows Runtime コンポーネントとして書いて、JS から呼び出す、ということが可能です。 言語選択の参考にしてください。

*1:もしかしたら自動で生成される機能があったりするかもしれないけど知らないです。

Windows 8.1 なら HTML + JS で書かれたストアアプリでも WebView が使用できる

Windows 8 向けの Windows ストアアプリの中に web ページを表示する必要があるとき、XAMLC#C++ などを使って開発する場合は WebView を使用することができました。 しかし、HTML + JS で開発する場合は、WebView の代わりに iframe を使うしかありませんでした。

Windows 8.1 では、HTML + JS でも WebView が使えるようになります! 非常にうれしいですね!!!

(Windows 8.1 Preview の情報をもとに記述しています。 Windows 8.1 の正式リリース版でも WebView は使えると思いますが、この記事はあくまで Preview 版での情報であることにご注意ください。)

iframe の代わりに WebView を使う

HTML 文書中に x-ms-webview というタグを記述することで WebView を設置できます。

<x-ms-webview id="webview" src="http://vividcode.hatenablog.com/" style="width: 640px; height: 800px; border: solid 1px white;"></x-ms-webview>

何が嬉しいの?

iframe ではいろいろと制約があったので、それらの制約がなくなることが嬉しいところです。

たとえば、はてなブログの HTTP レスポンスヘッダには X-Frame-Options: DENY が含まれているため、iframe 内に表示することはできませんでした。 WebView を使えば、そんな X-Frame-Options: DENY な web ページもストアアプリ中に表示できます。

あとはページの遷移などのナビゲーションをアプリのプログラム側から行えるようになるといったこともあります。

ブラウジングの機能: JS からの操作

たとえば以下のようにして前のページに戻すことができます。

var webview = document.getElementById("webview");
webview.goBack();
  • goBack()goForward() メソッドで履歴の移動
  • canGoBackcanGoForward プロパティで履歴の移動が可能かどうかわかる
  • あとは refresh() メソッドで更新、stop() メソッドで読み込み停止とか

XAML + C# などの WebView だとページ遷移の際に発火するイベントなども定義されていますが、ドキュメントに書かれてないので HTML + JS の WebView だと使えないかもしれないですね。し、JS では、XAML + C# などで使われるイベント名の頭に 「MSWebView」 をつけたイベント名でイベントを扱えるみたいです。

サンプルコードより: HTML WebView control sample (Windows 8.1) in C#, C++, JavaScript for Visual Studio 2013

var webviewControl = document.getElementById("webview");
webviewControl.addEventListener("MSWebViewNavigationStarting", navigationStarting);
webviewControl.addEventListener("MSWebViewContentLoading", contentLoading);
webviewControl.addEventListener("MSWebViewDOMContentLoaded", domContentLoaded);
webviewControl.addEventListener("MSWebViewNavigationCompleted", navigationCompleted);
webviewControl.addEventListener("MSWebViewUnviewableContentIdentified", unviewableContentIdentified);

そのほかにできること; 参考文献

Local state directories に web ページをダウンロードしてきてオフラインでも WebView 上に表示できるようにするとか、web ページのスクリーンショットを撮るとかできるみたいです。 詳しくは下記ドキュメントをご覧ください。