「.NET MAUI 8 で作成した APK が、ある端末では入るのに別端末では『安全ではありません → インストール出来ません』と弾かれる」。この現象は Target/Minimum SDK の整合、署名、ABI、Manifest の遺物が複合して起こることがほとんどです。本稿では最短解決策から、原因の切り分け、具体的な設定とコマンド、ログの読み方までを一気通貫で解説します。
まず結論(最短ルートの処方箋)
- テンプレートから新規 MAUI プロジェクトを作成し、コード・リソースだけ移植する(古い設定の持ち込みを避ける)。
- Target SDK=34(Android 14)、Minimum SDK=26(Android 8.0)に統一する。
- Release 用キーストアで署名してビルドする(Debug 署名は使わない)。
- サイドロード用の単一 APK なら必要な ABI をすべて含める、ストア配布ならAABを出力。
- うまく入らない端末では
adb logcatでインストール時ログを取り、エラーコードで対処を決める。
症状の典型例
同じ APK を配布しているのに、Android 8/11/12/14 の一部端末ではインストールできる一方、Android 10 端末や Android 14 の別個体では「提供元不明」「安全ではありません」「インストール出来ません」と表示され、インストーラ段階で拒否されることがあります。Google Play Protect の審査・端末のポリシー・OS バージョン固有の要件・APK 側のメタ情報(署名/ABI/SDK 指定)のいずれか、または複数が齟齬を起こしているサインです。
なぜ起こるのか(原因の全体像)
- Target SDK が低いと、近年の OS/Play Protect はリスク扱いし、サイドロード時に強い警告または拒否を出すことがある。
- ABI 不一致(端末が
arm64-v8aなのに APK にarmeabi-v7aしか入っていない等)はインストール不可。 - 署名の不備(Debug 署名/失効証明書/過去インストールとの証明書不整合)は OS がブロックする。
- Manifest や
csprojの重複・遺物(古い<uses-sdk>、不要なextractNativeLibs、installLocation等)が新 OS 要件と衝突。 - Android 12+では
intent-filterを持つコンポーネントにandroid:exportedが必須。欠落すると解析段階で弾かれる。
対策早見表(まずここから)
| 対策 | 具体策 | ねらい/効果 |
|---|---|---|
| 新規プロジェクトへ移植 | テンプレートで新規作成 → コード/画像/レイアウトのみ移す | 古い Manifest や csproj の地雷を除去し、最新既定値でクリーンに |
| SDK の適正化 | AndroidTargetSdkVersion=34、AndroidMinSdkVersion=26 | Play Protect の警告を回避、幅広い端末でインストール可 |
| Release 署名 | dotnet publish -c Release + キーストア指定 | 「提供元不明」/安全性警告の主要因を除去 |
| ABI の網羅 | android-arm; android-arm64; x86; x64 を指定、または AAB で配布 | ABI ミスマッチによる INSTALL_FAILED_NO_MATCHING_ABIS を防止 |
| ログで特定 | adb logcat で PackageManager/Installer を観測 | 「何が原因か」を 1 発で特定しムダ打ちを防ぐ |
| その他の衛生要件 | exported 必須、余分な uses-sdk は削除、旧版は一度アンインストール | Android 12 以降の新要件を満たす |
サンプル設定(.csproj)
テンプレートから作ったプロジェクトに対して、まずはこの最小構成をベースにします。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android</TargetFrameworks>
<UseMaui>true</UseMaui>
<ApplicationId>com.example.app</ApplicationId>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidMinSdkVersion>26</AndroidMinSdkVersion>
<AndroidTargetSdkVersion>34</AndroidTargetSdkVersion>
<RuntimeIdentifiers>android-arm;android-arm64;android-x86;android-x64</RuntimeIdentifiers>
<AndroidKeyStore>true</AndroidKeyStore>
<PublishTrimmed>true</PublishTrimmed>
ポイント:AndroidMinSdkVersion と AndroidTargetSdkVersion を csproj に統一して定義します。AndroidManifest.xml 側の <uses-sdk> は基本的に記述しない(重複・競合の原因)。
署名付き Release ビルドと検証
Debug 署名の APK は近年の端末で強い警告対象になり、場合によってはインストール拒否のトリガになります。必ず Release 署名でビルドしましょう。
# 署名付き APK を出力
dotnet publish -f net8.0-android -c Release ^
-p:AndroidKeyStore=true ^
-p:AndroidSigningKeyStore=.\keystore\myapp.jks ^
-p:AndroidSigningKeyAlias=myapp ^
-p:AndroidSigningKeyPass=%KEY_PASS% ^
-p:AndroidSigningStorePass=%STORE_PASS%
# 署名の検証(Android SDK の apksigner を使用)
apksigner verify -v .\bin\Release\net8.0-android\publish*.apk
# 端末へインストール(失敗時は -t でテストオンリー、-r で上書き)
adb install -r .\bin\Release\net8.0-android\publish\myapp.apk
過去に異なる証明書で同じパッケージ名のアプリを入れていた場合、INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES で失敗します。まず旧版をアンインストールしてください。
# 既存の同一パッケージを削除(データも消えます)
adb uninstall com.example.app
Manifest の掃除と必須属性
MAUI の Manifest は Platforms/Android/AndroidManifest.xml に配置されます。テンプレートから大きく逸脱しないのが鉄則です。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<application android:label="MyApp" android:supportsRtl="true">
<activity android:name="crc64a0e0a82d0db9a07d.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<!-- ※ 原則 <uses-sdk> はここに書かない(csproj に集約) -->
</manifest> </code></pre>
<ul>
<li><strong>Android 12+</strong>:<code>intent-filter</code> を持つ <code>activity/service/receiver</code> は <code>android:exported</code> が必須。欠落すると解析時に即失敗します。</li>
<li><code>android:extractNativeLibs="false"</code> を古い端末で入れていると、稀にベンダー実装で問題化します。属性ごと削るか、まずは既定(未指定)に戻して検証を。</li>
<li><code>android:installLocation="auto"</code> は SD 優先で空き容量判定に引っかかることがあります。不要なら付けない。</li>
</ul>
</section>
<section>
<h2>ABI(CPU アーキテクチャ)の整合</h2>
<p>端末の CPU と APK に含めたネイティブライブラリの ABI が一致しないとインストールできません。サイドロード配布の「単一 APK」は、<strong>必要 ABI をすべて同梱</strong>するのが確実です。</p>
<pre><code><RuntimeIdentifiers>android-arm;android-arm64;android-x86;android-x64</RuntimeIdentifiers>
</code></pre>
<p>ストア配布(Google Play)の場合は <strong>AAB</strong> にすると、配布時に端末別スプリットが自動最適化されます。</p>
<pre><code># AAB を生成
dotnet publish -f net8.0-android -c Release -p:AndroidPackageFormat=aab
</code></pre>
<p>ローカル検証では <code>bundletool</code> を用いると、指定 ABI・言語・画面密度向けの APKS を生成して端末へ配布できます。</p>
</section>
<section>
<h2>インストール失敗ログの読み方(原因→対処 対応表)</h2>
<p>再現端末でインストール操作を行いながら、別コンソールで <code>adb logcat</code> を流します。<code>PackageManager</code> / <code>PackageInstaller</code> / <code>ActivityManager</code> タグを中心に確認します。</p>
<pre><code># 例:インストール時ログを抽出
adb logcat | findstr /R /C:"PackageManager" /C:"PackageInstaller" /C:"ActivityManager"
</code></pre>
<table>
<thead>
<tr>
<th>代表的なログ/エラー</th>
<th>意味</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>INSTALL_FAILED_NO_MATCHING_ABIS</code></td>
<td>端末の CPU と APK の ABI が不一致</td>
<td><code>RuntimeIdentifiers</code> に不足 ABI を追加。AAB なら端末に合った Split を配る</td>
</tr>
<tr>
<td><code>INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES</code></td>
<td>既存インストールと証明書が違う</td>
<td>旧版をアンインストール、または過去と同じキーストアで署名</td>
</tr>
<tr>
<td><code>INSTALL_FAILED_VERSION_DOWNGRADE</code></td>
<td>インストール済みより低い versionCode</td>
<td><code>VersionCode</code> を増やす(MAUI では <code>ApplicationVersion</code>/<code>ApplicationDisplayVersion</code>)</td>
</tr>
<tr>
<td><code>INSTALL_PARSE_FAILED_MANIFEST_MALFORMED</code></td>
<td>Manifest の記述不備(例:<code>exported</code> 無し)</td>
<td>Android 12+ 要件に合わせて修正。不要な <code><uses-sdk></code> は削除</td>
</tr>
<tr>
<td>警告:「安全ではありません」「提供元不明」</td>
<td>Debug 署名、Target SDK が低い等で Play Protect が警告</td>
<td>Release 署名に切替、Target=34 を明示、信頼できる配布経路に統一</td>
</tr>
<tr>
<td><code>INSTALL_FAILED_INVALID_APK</code></td>
<td>不正な構造/壊れた APK</td>
<td>ビルド時のキャッシュをクリアして再発行(<code>dotnet clean</code>、<code>bin/obj</code> 削除)</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>「新規プロジェクトへ移植」の実践手順</h2>
<ol>
<li>
<p><strong>新規に MAUI テンプレートを作成</strong></p>
<pre><code>dotnet new maui -n MyApp.Clean
cd MyApp.Clean
最低限の設定を追加(前掲 .csproj のとおり)
コードとリソースを段階移植
View/ViewModel/Services等の C# を移す。- 画像は
Resources/Images、フォントはResources/Fonts、Raw はResources/Rawに配置。 - 古い
AndroidManifest.xmlの個別設定は必要最小限だけ持ち込む。
1 機能ずつビルド&サイドロードして、再現端末で動作確認(ログも並走)。
このプロセスは面倒に見えて、実は最短で事故要因を除去できます。テンプレート既定値は最新のビルドパイプラインや OS 要件に追随しており、古い設定の混入を避けること自体が最大のチューニングです。
Android 10/14 個体差で弾かれるときの追加観点
- 企業端末/セキュリティアプリ:サイドロードを MDM/セキュリティアプリが禁止している場合は、社内ポリシーの例外申請が必要。
- タイムスタンプ/日付:端末の日付が極端にズレていると署名検証で弾かれることがあります。
- 暗号化/ストレージ空き:
installLocationの影響で SD を優先し、空き不足で失敗することがあります。 - 安全でないソースの APK と検知される場合、同一の配布経路(社内ストレージや MDM)に統一すると通りやすくなります。
デバイス/OS 版ごとの推奨設定マトリクス
| OS | 推奨 Target / Min | 備考 |
|---|---|---|
| Android 8.0–8.1(API 26–27) | Target=34 / Min=26 | Min=26 で網羅。古いベンダー機は extractNativeLibs 未指定が安定 |
| Android 9–10(API 28–29) | Target=34 / Min=26 | 個体差あり。署名と ABI を厳密に |
| Android 11–13(API 30–33) | Target=34 / Min=26 | Debug 署名は強警告。Release 署名で |
| Android 14(API 34) | Target=34 / Min=26 | Target が 34 未満だと警告/拒否の温床 |
Play Protect 警告への向き合い方
Play Protect の「安全ではありません」は、署名・配布経路・Target SDK の組み合わせで発火します。回避には以下が有効です。
- Release 署名の APK/AAB のみ配布する。
- Target SDK=34。古い Target は「古いセキュリティモデル」と見なされやすい。
- 配布経路を社内ストレージ/MDM など管理下の一元経路に限定し、ハッシュ値を提示して整合性を確認できるようにする。
よくある「地雷」まとめ
- Manifest と csproj の二重指定:
<uses-sdk>を Manifest に書き、AndroidMin/TargetSdkVersionも csproj に書くと齟齬の温床。 - 証明書のローテーション:パッケージ名を据え置いたままキーストアを変えると上書きインストールができない。旧版削除が必要。
- ABI の取りこぼし:サイドロードの単一 APK で
arm64-v8aを落とすと最新機で必ず失敗。 - versionCode の据え置き:インクリメントを忘れると「ダウングレード」扱いで失敗。
- Android 12 の
exported:intent-filter を持つのに未指定だと即死。
CI/CD での安定ビルド例(GitHub Actions 概要)
name: Build Android Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Publish (APK)
run: dotnet publish MyApp.csproj -f net8.0-android -c Release ^
-p:AndroidKeyStore=true ^
-p:AndroidSigningKeyStore=$(KeystorePath) ^
-p:AndroidSigningKeyAlias=$(KeystoreAlias) ^
-p:AndroidSigningKeyPass=${{ secrets.KEY_PASS }} ^
-p:AndroidSigningStorePass=${{ secrets.STORE_PASS }}
- name: Artifact
uses: actions/upload-artifact@v4
with:
name: apk
path: bin/Release/net8.0-android/publish/*.apk
ビルドサーバに Android SDK/NDK を過不足なく入れる必要はありますが、MAUI 8 の場合は dotnet workload install maui で必要ツールチェーンが揃います。キーストアはリポジトリに置かず、ストレージ/シークレットから取得してください。
トラブル時のフローチャート(テキスト版)
- テンプレートから新規作成 → 最小設定(Target=34/Min=26/Release 署名/ABI 充足)。
- 再現端末 A にインストール → 失敗したら
adb logcatを確認。 - エラーコードに応じて:
NO_MATCHING_ABIS→ ABI 追加。INCONSISTENT_CERTIFICATES→ 旧版アンインストール or 証明書合わせ。MANIFEST_MALFORMED→exported・重複uses-sdk・不要属性を修正。- 警告で止まる → Release 署名/Target=34 を再確認。
- 成功したら元プロジェクトとの差分を比較し、持ち込む設定を精査。
補足:アプリバージョンの付け方(MAUI)
上書きインストールを成功させるため、ビルドごとに 内部バージョン(versionCode)を単調増加させます。
<PropertyGroup>
<ApplicationDisplayVersion>1.2.3</ApplicationDisplayVersion> <!-- 表示用 -->
<ApplicationVersion>45</ApplicationVersion> <!-- 内部番号(整数) -->
</PropertyGroup>
ケーススタディ:Android 10 機でだけ「安全ではありません」→失敗する
あるプロジェクトでは、Target SDK=30 のままビルドしており、Android 10 の一部個体と Android 14 の特定機でサイドロード時に Play Protect が強いブロックを掛け、インストーラ UI でキャンセル扱いになっていました。新規プロジェクトへ移植して Target=34/Min=26 に引き上げ、Release 署名・arm64 を含めた単一 APK に統一したところ、すべての検証端末でインストールが通過しました。Manifest はテンプレート準拠に戻し、exported を明記したのも奏功しました。
チェックリスト(公開前の最終確認)
| 項目 | 確認内容 |
|---|---|
| SDK | AndroidTargetSdkVersion=34 / AndroidMinSdkVersion=26 |
| 署名 | Release 用キーストアで署名(期限・アルゴリズム OK) |
| ABI | サイドロード APK は arm64-v8a を必ず含む(必要なら armeabi-v7a 等も) |
| Manifest | exported 必須箇所に付与、不要な <uses-sdk> や旧属性を削除 |
| バージョン | ApplicationVersion(versionCode)を毎回インクリメント |
| 競合アプリ | 同一パッケージ名の旧版を削除してから検証 |
| ログ | adb logcat でインストール時ログを取得し、エラーが残っていないか |
| 配布形式 | 社内配布=署名 APK、ストア配布=AAB |
まとめ:いちばん速いのは「作り直して移す」
MAUI は進化が速く、テンプレート既定値に多くの最適化が積み込まれています。新規プロジェクトを作り、Target SDK=34・Min SDK=26・Release 署名・必要 ABI 充足という基本を押さえるだけで、端末間の「インストール出来ません」がほぼ解消します。残る個体差は adb logcat のエラーコードに素直に従って潰していけば、短時間で安定配布へ到達できます。
付録:便利コマンド集
# 端末の ABI を確認
adb shell getprop ro.product.cpu.abi
adb shell getprop ro.product.cpu.abilist
# 端末の OS / セキュリティパッチ
adb shell getprop ro.build.version.release
adb shell getprop ro.build.version.sdk
# アプリ情報(パッケージ、バージョン、署名)
adb shell dumpsys package com.example.app | more
# 失敗時の直近ログだけ見る
adb logcat -v time | findstr /R /C:"PackageManager" /C:"PackageInstaller"
# クリーンビルド
dotnet clean
rd /S /Q .\bin .\obj
# AAB→APKS のローカル配布(bundletool)
java -jar bundletool.jar build-apks --bundle=app.aab --output=app.apks --connected-device
java -jar bundletool.jar install-apks --apks=app.apks
付録:よくある質問(抜粋)
Q. Android 14 端末の一部でだけブロックされます。
A. Target=34 未満・Debug 署名は強い警告対象。まずは Target=34/Release 署名に統一。次に ABI と Manifest(exported)を再確認。
Q. arm64 だけ入れても大丈夫?
A. 実機はほぼ arm64-v8a ですが、古い機種や一部エミュ用に v7a/x86 が必要なケースがあります。サイドロードの単一 APK で広く対応するなら全 ABI を同梱、それ以外は AAB を推奨。
Q. 旧版からの上書きができません。
A. 証明書が変わっていないか、ApplicationVersion(内部番号)が上がっているかを確認。変わっているなら旧版アンインストールが必要です。

コメント