読者です 読者をやめる 読者になる 読者になる

【Android】ブラウザからのIntentの送信とアプリがインストールされてない場合のフォールバック

前書き

ここ半年ほどコードよりも日本語を書く仕事がメインになっていて、それはそれは退屈かつシビアなものだったのですが、最近はまたちょこちょこコードを書く仕事をしています。楽しいです。

色々な都合から以前のように一日に何本も記事を書いたりはできないんですが、現状、一ヶ月に一本ペースになってしまっているので、一週間に一本ぐらいのペースにしたいですね。

で、今回はタイトルの通り、AndroidのブラウザからIntentを送信する方法と、対応するアプリがインストールされていない場合のフォールバック方法について解説します。

Intentを送信するのは至極簡単なんですが、フォールバックは中々厄介です。

アプリ側でそのIntentを受信する方法は以前書いたので、今回は特に説明しません。リンクぐらいは貼っておきますが。

Intentの送信方法

Chromeの資料にさらっと書かれています。synraxの箇所を引用してみましょう。

HOST/URI-path // Optional host 
#Intent; 
   package=[string];
   action=[string]; 
   category=[string]; 
   component=[string]; 
   scheme=[string]; 
end;

例がないとわかりにくいですね。アプリ側はこんなAndroidManifest.xmlだとします。

    <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="hoge.fuga.piyo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="4"
        android:targetSdkVersion="10" />
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <activity android:name="hoge.fuga.piyo.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name="hoge.fuga.piyo.IntentActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="hogefuga" android:host="piyo"/>
            </intent-filter>
        </activity>

    </application>

</manifest>

上記の例で設定したIntentActivityのintent-filterに引っかかるようにするためには、このようなURIを用意してあげます。

intent://piyo#Intent;package=hoge.fuga.piyo;action=view;category=browsable;scheme=hogefuga;end;

また、一応こんなURIでも呼べますが、今はあまり推奨されていないようです。actionやcategoryも指定できませんしね。

hogefuga://piyo

アプリがインストールされていない場合のフォールバック

どちらかと言うと、これが本題。

「WebからネイティブアプリをIntentで起動させたい!」と言う要件。当然あると思います。

が、先ほどの例でのhogefuga://piyoなんてURIだと、ネイティブアプリがインストールされていない端末では「ページが見つかりません」なんてつれないことを言われてしまいます。

ってなわけで、「インストールされていなかったら別のページに移動させたい!」となることも多いでしょう。どーやってやるかを解説していきます。

packageを指定してインストールされていなかったらGoogle Playに飛ばす

IntentのURIでわざわざpackageを指定していましたが、あれがフォールバックそのものになります。

Android君は対象となるintent-filterが見つからないと自分で「インストールされてないっぽいからGoogle Playに飛ばすか!」とやってくれます。ありがたいですね。Google Playにおける各アプリのインストール画面はパッケージ名をクエリとして持っている(例えばTwitterならcom.twitter.android)ため、このような芸当ができるわけですね。ありがたいことです。

が、この方法、何らかの事情でGoogle Playに公開していないアプリでは何の意味もありません。

むしろ意味なくGoogle Playに飛ばしてしまうので、野良アプリの場合はURIからpackageの指定を外しておいた方がいいです。(それでもIntent自体は動いてくれます。)

S.browser_fallback_urlを指定する

先ほどのドキュメントを読み返してみると、S.browser_fallback_urlと言う最早そのまんまな名前のパラメータに関する記述があります。

指定方法も簡単で、エンコード済みのURIを渡すだけです。先ほどのURIからpackageを外してS.browser_fallback_urlを指定してみましょう。例えば、http://hoge.fuga.piyo/app-install.htmlなんて場所に飛ばしたければこんな感じです。

intent://piyo#Intent;action=view;category=browsable;scheme=hogefuga;S.browser_fallback_url=http%3a%2f%2fhoge%2efuga%2epiyo%2fapp%2dinstall%2ehtml;end;

で、やってみると本当に飛ばしてくれます。最新版のChromeなら。

この方法は非常にスマートなんですが…その…Chromeが対応したのが本当に最近で、今年の3~4月頃にリリースされた新機能だったりします。(細かいバージョンはちゃんと調べていません…。)

一応AndroidのWebViewもChromiumを使っていることには間違いないので、そのうち対応されるんでしょうが、現状では常にChromeを最新版にしていてなおかつ普段のブラウジングもChoromeしか使わない人にしか機能しません。つまり、実質使えないってことですね。

JavaScriptを書いてどうにかする

こうなったらJavaScriptだ!ってことで、頑張って書いてみました。

function fallbackApp(uri, fallbackUri) {
    // 起動する瞬間の時間を取得しておく
    var startTime = new Date().getTime();
    
    // 普通にlocation.hrefにuriを渡すとその後操作できないので
    // window.openを使って開く
    var w = window.open(uri, "intent");
    
    setTimeout(function() {
        if(w) {
            // ブラウザに処理が戻ってきたタイミングの時間を取得する
            var endTime = new Date().getTime();
            
            // アプリが起動しようとしまいと、ブラウザに処理が戻ってきた
            // タイミングでここのsetTimeoutは動く。
            // そこで、起動時と起動後の時間の差分をとって無理矢理判定する。
            // (そうしないと、アプリが起動したのにフォールバックしてしまう。)
            if((endTime - startTime) < 3000)) {
                w.location.href = fallbackUri;
            }
        }
    }, 1000); // ここの時間は多分もっと短くても良い。
              // 1秒だと「ページが表示されませんでした」っぽいエラーが普通に見えてしまう。
}

見てもらえばわかる通り、相当な力技です。

IntentのためのURIはどう頑張ってもsame origin policyに反するので(そもそもschemeが違うんだから打つ手がない)、このようにsetTimeoutで逃げるしかありませんでした。せめてw.documentあたりが取得できればまた違ったのでしょうが…。

2015/12/11追記:標準ブラウザでのフォールバック

上記のスクリプトですが、Chromeでは動くものの、Androidの標準ブラウザでは動作しません…。

試行錯誤の結果、更なるパワープレイで対処できました。

// intentを飛ばすためのa要素を作成し、bodyにappendする
var a = document.createElement("a");
a.href = url;
a.target = "_blank"; // 同ウィンドウ内でアプリを開くとフォールバックしないので無理矢理別ウィンドウに…
var body = document.getElementsByTagName("body")[0];
body.appendChild(a);

// a要素のクリックイベントをぶったたく
var e = document.createEvent("MouseEvents");
e.initMouseEvent("click", true, true, window, 0,0,0,0,0,false,false,false,false,0,null);
a.dispatchEvent(e);

// 一定時間内にアプリが起動しなかったらフォールバック
setTimeout(function() {
	var endTime = new Date().getTime();
	if((endTime - startTime) < 3000) {
		window.location = fallbackUrl;
	}
}, 1000);

こうすると別ウィンドウでは「ページが見つかりません」みたいな表示がされ、リンク元のウィンドウでフォールバック、というあんまりイケてない感じにできます。文句はGoogleに言ってください。

まとめ

そんなわけで、Intentの送信は簡単なんですが、フォールバックさせるのは死ぬほどしんどいよ、と言うお話でした。

参考