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

2018/12/01 追記

前書き

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

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

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

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

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

outofmem.hatenablog.com

outofmem.hatenablog.com

Intent の送信方法

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

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 の送信は簡単なんですが、フォールバックさせるのは死ぬほどしんどいよ、と言うお話でした。

参考