何が起きたのか
アプリが暗黙的インテントで startActivity(intent) を呼び出したとき、Androidは対応できるアプリを見つけられませんでした。要求したインテントフィルターを宣言しているインストール済みアプリが存在しないのです。logcatには以下のようなクラッシュが表示されます:
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=https://example.com }
at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2105)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1747)
at android.app.Activity.startActivity(Activity.java:5258)
これはパーミッションエラーでも、あなたのマニフェストの設定ミスでもありません。デバイス上のどのアプリも、要求した内容のインテントフィルターを宣言していないのです。それでも、動作するはずの環境でもこれが起きてしまう、いくつかの具体的な原因があります。
デバッグの手順
1. 発行されている正確なインテントを確認する
startActivity を呼び出す前にインテントをログ出力しましょう:
Log.d("IntentDebug", intent.toString());
3点を注意深く確認してください:アクションは正しいか?URIスキームは適切か(https:// と http://、または myapp:// のようなカスタムスキームの違い)?MIMEタイプが不要なのに設定されていないか、またはハンドラーが必要とするのに省略されていないか?
2. 処理できるアプリがあるか確認する
startActivity() を呼び出す前に必ず resolveActivity() を呼びましょう:
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
} else {
Log.e("IntentDebug", "No handler found for: " + intent.toString());
Toast.makeText(this, "No app found to handle this action", Toast.LENGTH_SHORT).show();
}
nullが返ってきた場合、問題が確認できます。Android 11以降では、対象アプリがインストールされているにもかかわらず、アプリから見えない状態になっているためnullになることもあります。詳しくは後述します。
3. Android 11以降のパッケージ可視性(落とし穴)
API 30からパッケージ可視性フィルタリングが導入されました。ChromeやGmailがインストールされていても、マニフェストの <queries> ブロックにインテントを宣言しなければ、アプリはそれらを照会できません。このブロックを省略すると、ブラウザが正常にインストールされているデバイスでも resolveActivity() はnullを返します。Android 11以降で最も多い原因がこれです。
解決策
修正1:AndroidManifest.xmlに を追加する(Android 11以降)
アプリが照会する必要のあるインテントパターンを宣言します。最もよく遭遇する3つを挙げます:
<!-- ブラウザでURLを開く -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
<!-- メールを送信する -->
<queries>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<!-- 電話番号をダイヤルする -->
<queries>
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
</queries>
単一の <queries> 要素内に複数の <intent> ブロックを並べることができます。配置に関する重要な注意点として、<queries> は <application> の内側ではなく、<manifest> の直接の子要素でなければなりません。
修正2:startActivityをtry/catchで囲む
<queries> を追加していても、防御的なtry/catchはエッジケースを救います。LavaやTecnoなどのメーカーが出荷する削減されたOEM ROMを搭載したAndroid廉価端末では、デフォルトのブラウザやメールクライアントが存在しない場合があります:
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"));
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, "No browser installed", Toast.LENGTH_SHORT).show();
}
Kotlinの場合:
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, "No app found to open this link", Toast.LENGTH_SHORT).show()
}
修正3:共有にはIntent.createChooserを使う
ACTION_SEND の場合、チューザーは通常クラッシュの代わりに空のダイアログを表示します。しかし、API 30の可視性ルールによってアプリが1つも見えない場合は、依然として例外が発生します。両方のケースに対処しましょう:
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, "Check this out!");
Intent chooser = Intent.createChooser(shareIntent, "Share via");
if (shareIntent.resolveActivity(getPackageManager()) != null) {
startActivity(chooser);
}
マニフェストに対応するクエリを追加します:
<queries>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent>
</queries>
修正4:ハンドラーのないカスタムURIスキーム
myapp://action のようなカスタムスキームは、対象アプリがインストールされていない場合に完全に失敗します。発行前に確認し、Webへのフォールバックを用意しておきましょう:
Uri customUri = Uri.parse("myapp://open/profile/123");
Intent deepLink = new Intent(Intent.ACTION_VIEW, customUri);
if (deepLink.resolveActivity(getPackageManager()) != null) {
startActivity(deepLink);
} else {
// アプリが未インストール — Webにフォールバック
Intent fallback = new Intent(Intent.ACTION_VIEW, Uri.parse("https://myapp.com/profile/123"));
startActivity(fallback);
}
動作確認
実機でアプリを実行してください。エミュレーターは実際のハードウェアでしか現れない可視性の問題を隠してしまうことがあります。以下の4点を確認しましょう:
- インテントがクラッシュせずに解決される。
- logcatに
ActivityNotFoundExceptionのスタックトレースが表示されない。 - 可視性ルールが適用されるAndroid 11以降(API 30)のデバイスでも動作する。
- 最小限のアプリ構成のデバイスやGAppsなしのエミュレーターイメージでフォールバックパスが正しく機能する。
<queries> ブロックが機能しているか確認したい場合は、実行時にアプリから実際に見えているアプリの一覧を出力しましょう:
// Kotlin — https に対して ACTION_VIEW を処理できるアプリを一覧表示
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
val activities = packageManager.queryIntentActivities(intent, 0)
activities.forEach { Log.d("PM", it.activityInfo.packageName) }
<queries> を追加したのにリストが空の場合は、スキームを再確認してください。https と http は別のエントリとして扱われ、それぞれ個別に宣言する必要があります。
教訓
- nullチェックやtry/catchのない暗黙的インテントは時限爆弾です。 Androidデバイスの多様性は容赦ありません。Chromeでさえ、インストールされていると決めつけることはできません。
- Android 11以降はパッケージ可視性をオプトイン方式に変更しました。 アプリが発行するすべての暗黙的インテントに対して、対応する
<queries>エントリが必要です。これがないと、対象アプリがデバイスにインストールされていてもresolveActivity()はnullを返します。 - 工場出荷状態のリセットや廉価端末はフォールバックロジックの穴を露わにします。 廉価スマートフォンにはデフォルトのブラウザ、メールクライアント、マップアプリがない場合が多くあります。フォールバックがコードのコメントだけなら、それはフォールバックとは言えません。
- 役立つメンタルモデルとして:
<queries>はパーミッションとは関係ありません。Androidにアクセスを求めているのではなく、照会時にアプリが認識すべきインテントフィルターをAndroidに伝えているのです。

