他アプリ起動周りでちょっとハマったのでメモ.
テキストやURIを暗黙Intentで共有する場合, 自アプリがそれに反応するintent-filter
を持っていると, ActivityChooser
に表示候補として含まれてしまう場合があります.
自アプリで捌きたくないから他アプリに共有しているのに, そのリストに自アプリが載っているのはよろしくない.
ということで, Intentは投げるけれどActivityChooser
に自アプリを含めない方法を探りました.
TL;DR
- createChooser, ChooserActivityまわりの挙動がOSバージョンで異なっている
- API LV.23 前後で
PackageManager.MATCH_DEFAULT_ONLY
の振る舞いが変わる - API LV.23 前後でActivity選択ダイアログのレイアウトが変わる
- 結論
queryIntentActivities
からの自前ダイアログ生成のが楽そう
シンプルにqueryIntentActivities
とIntent.createChooser
を組み合わせればできるだろうと思っていたのですが, 古いOSで確認したところ意図した通りに動きませんでした.
で, 古いOSでの動作もサポートすべく, 色々検討した結果を残しておきます.
createChooser
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL
: PackageManager.MATCH_DEFAULT_ONLY;
List<ResolveInfo> launchers
= context.getPackageManager().queryIntentActivities(intent, flag); // *a
// 自アプリを起動対象から除外する
List<Intent> intents = new ArrayList<>();
for (ResolveInfo app : launchers) {
if (context.getPackageName().equals(app.activityInfo.packageName)) {
continue;
}
Intent target = new Intent(intent);
target.setPackage(app.activityInfo.packageName);
intents.add(target);
}
if (intents.isEmpty()) {
// 起動対象のアプリが見つからなかった
} else {
// createChooserの第一引数のIntentに反応できるアプリが存在しない場合は EXTRA_INITIAL_INTENTS
// の指定が無視されるため, 必ず反応できるIntentを設定する目的でremove(0)を指定する.
Intent chooser = Intent.createChooser(intents.remove(0), title); // *1
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[0])); // *1
context.startActivity(chooser);
}
ポイントは *1 の部分で, 下記のコードではAPI Lv.23未満だとうまく動作しませんでした.
Intent chooser = Intent.createChooser(new Intent(), title); // *1
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[0])); // *2
EXTRA_INITIAL_INTENTS
に目的のIntentを設定すればうまくいきそうなものですが, API Lv.23未満だと *1 の第一引数Intent
に反応できるActivityの数が0であった場合に EXTRA_INITIAL_INTENTS
が無視される挙動になります(つまりActivityNotFound)
API Lv.23以上ではEXTRA_INITIAL_INTENTS
が評価されます.
API Lv.23未満でcreateChooser
の第一引数に渡すIntentは, 少なくとも1つ以上のActivityが反応できる必要があるので下記のようなコードになりました.
Intent chooser = Intent.createChooser(intents.remove(0), title); // *1
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[0])); // *2
MATCH_ALL
*a で, PackageManager.MATCH_DEFAULT_ONLY
はAPI Lv.23から挙動が変わっています.
API Lv.23未満だと, Category.DEFAULT
に反応するActivityを抽出するものでしたが,
API Lv.23以上だと, 「既定で開く」設定されたActivityがある場合はそのActivityしか返却されなくなりました. API Lv.23以上でAPI Lv.23未満と同じ挙動にするためにはAPI LV.23から追加されたPackageManager.MATCH_ALL
を指定する必要があります.
int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL
: PackageManager.MATCH_DEFAULT_ONLY;
List<ResolveInfo> launchers
= context.getPackageManager().queryIntentActivities(intent, flag); // *a
より便利にいくなら, API Lv.23以上でもMATCH_DEFAULT_ONLY
でResolveInfo
を拾って, 「既定で開く」設定が自アプリになっていなければそのまま起動, 自アプリであれば上記の処理を実行するとすればいけそうです.
この処理でうまくいきましたが, デバイスによってはシェアダイアログのレイアウトが下記のように残念な結果に :(
動作をみる限りでは, createChooser
に渡したIntentが1行目に並び, EXTRA_INITIAL_INTENTS
に渡したIntentが2行目に並んでいる様子.
これを解決するならシェアダイアログを自前で組む必要がありそうです.
(あるいはAPI Lv.23ではcreateChooser
の第一引数にどのActivityにもマッチしないnew Intent()
といったIntentを指定するなど…)
API Lv.24からEXTRA_EXCLUDE_COMPONENTS
なる定数も追加されているので, API Lv.24以上はこれを使えということかもしれませんが, こんなことにOSバージョン分岐させるのも面倒なので, 手っ取り早くやるならqueryIntentActivities
からの自前ダイアログ作成が安定しているという結論に落ち着きました.
以上です.