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

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

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

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

Chrome からの共有で onNewIntent が呼ばれない問題 (Android アプリの documentLaunchMode の話)

Android アプリ

AndroidAPI level 21 で導入された documentLaunchMode に関する Activity の挙動にバグっぽいところがあって、結構扱いに困るのでまとめておきます。 「documentLaunchMode? 関係ないや」 って思ってる人でも、外部アプリからの Intent を扱うアプリを書くときに影響されるかもしれません。

まとめ

  • Activity の documentLaunchMode として intoExisting が指定されており、既存の Activity が再利用される場合は、launchModestandard であっても Activity の onNewIntent メソッドが呼ばれるべき *1 だが、実際には呼ばれない。
    • バグっぽい。
  • AndroidManifext.xmldocumentLaunchMode の値を指定していなくても、外部アプリが投げる Intent に intoExisting 相当のフラグ (FLAG_ACTIVITY_NEW_DOCUMENT フラグ) が設定されていることがあるので、外部からの Intent を受け取る Activity を書いている場合には否が応でもこの問題に悩まされる。
    • 例えば Chrome アプリの 「共有」 が投げるインテントには FLAG_ACTIVITY_NEW_DOCUMENT フラグが設定されている。
  • とりあえずの対処法は、AndroidManifest.xml で、Activity に launchMode="singleTop" を指定すること。
    • アプリ内部で使用することを考えて singleTop にしづらい場合は、内部で使用する Activity と外部からの Intent を受け取るための Activity を別に定義して (単純にサブクラスを作れば良いだろう)、外部からの Intent を受け取るための Activity の launchModesingleTop にする、などの対応が必要。

関連する発表資料

この記事の内容に関連した話を 2015 年 9 月 30 日の 「関西モバイル研究会 #6」 で発表しました。

背景

この問題に行きついた背景です。

  • 外部アプリからテキスト (Intent.EXTRA_TEXT) を含む Intent を受け取って処理する Activity を含むアプリを開発していた。
    • Chrome アプリの 「共有」 で投げられる Intent も受け取れる。
  • 次のようなユーザー操作を行うと、Activity が新しい Intent を扱えなくて困った。
    1. Chrome でとあるページ (例として 「http://example.com/1」) で 「共有」 し、Activity を起動。 → Activity では Intent から 「http://example.com/1」 というテキストを取りだせる。
    2. Activity を終了せずに、アプリを切り替えて Chrome に戻る。
    3. 別のページ (例として 「http://example.com/2」) で再度 「共有」 し、同じ Activity を起動。 → Activity が破棄されずに残っていた場合、onStart メソッドonResume メソッドが呼ばれるが、onNewIntent メソッドが呼ばれず、getIntent メソッドで取得できる Intent も前に開いたときの Intent になっている。 → 新しい Intent の情報が得られない!

documentLaunchMode の話

documentLaunchMode とはなんぞや、という話。

  • 公式ドキュメントとしては以下のページを読むとわかりやすいです。
  • API level 21 で導入。
  • Activity 起動時の挙動を制御するためのもの。
    • Activity 起動時に毎回新しいタスクを生成するようにしたり、コンポーネント情報と data URI 情報が同じ場合は同じタスクを使うようにしたり、みたいな指定ができる。
  • これを指定することで、「最近のタスク一覧」 に同じアプリケーションの複数ドキュメントを表示できる。
  • AndroidManifest.xml で指定することもできるし、startActivity にフラグとして渡すこともできる。

intoExisting

  • documentLaunchMode の値として intoExisting を指定すると、既存のすべてのタスクの中から Intent のコンポーネント情報と dataURI が同じものが探される。
    • あれば、そのタスクの中身がリセットされて、Activity が再利用されて Activity の onNewIntent メソッドが呼ばれる。
    • なければ、新しいタスクが生成される。
  • と、ドキュメントには書かれているが、onNewIntent メソッドが呼ばれるのはタスクの Activity の launchMode として singleTop などの値が指定されている場合で、standard では呼ばれなかった。 (まじかよ……)
  • DocumentCentricApps というサンプルコードがあるが、サンプルコードのコメントなどはドキュメント通りの挙動を期待しているが、実際の挙動はドキュメントどおりにはならなかった。 (Nexus 5; Android 5.1.1 で確認)

バグっぽい

Chrome アプリの 「共有」 で onNewIntent が呼ばれない問題

原因

Chrome アプリの 「共有」 (メニューの 「共有」 の右側に表示されている、最後に起動したアプリアイコンをタップ) で投げられる Intentを見てみたところ、以下のフラグが設定されていました。

  • FLAG_ACTIVITY_PREVIOUS_IS_TOP
  • FLAG_ACTIVITY_FORWARD_RESULT
  • FLAG_GRANT_READ_URI_PERMISSION
  • FLAG_ACTIVITY_NEW_DOCUMENT (API level 21; documentLaunchMode="intoExisting" 相当のフラグ)

上述のとおり、documentLaunchModeintoExisting で、launchModestandard だと、古い Activity が再利用されるもの onNewIntent メソッドが呼ばれないのです。 つらいですね。

回避策

一つの解決策としては、Activity に launchMode="singleTop" を設定する方法があります。 これだと onNewIntent メソッドが呼ばれるようになります。

しかし、外部からの Intent を受け取るだけじゃなくてアプリ内部からも起動される Activity の場合は launchMode="singleTop" を設定することができないことも多いでしょう。 そういう場合は、アプリ内部からしか起動されない Activity と外部からの Intent を受け取る Activity を別のクラスにしてしまって (片方をもう一方のサブクラスとするなど)、外部からの Intent を受け取る Activity にだけ launchMode="singleTop" を設定するなどの方法を採ることになるでしょうか。 もうちょっといい方法があれば嬉しいですが、それぐらいしか思いつきませんでした。

終わり

この問題、とにかく辛いのですがあんまり困ってる人を見かけないので、もしいい感じの回避策があるのでしたら教えてください!!!!

*1:ドキュメントを読む感じだとそうだと思われる