.NET MAUIで社内配布APKをアプリ内更新する方法|FileProviderでインストール画面を起動

社内配布(ストア外配布)の .NET MAUI(Android)アプリを、端末上で最新版へ更新したい。APK のダウンロードはできるのに、ダウンロード後の「インストール(更新)」をアプリ内からどう起動すればいいか分からない――本記事では、Android の制約を踏まえた現実的な実装手順と、つまずきやすいポイントをまとめます。

目次

.NET MAUI アプリで「ストア外配布 APK」をアプリ内から更新したいときの結論

最初に結論です。Google Play を使わない社内配布 APK を、一般的な Android アプリ(.NET MAUI を含む)から無操作でサイレント更新することは基本的にできません。できるのは、APK を端末に保存したうえで、Android のインストール画面(パッケージインストーラ)を起動し、ユーザーに確認してもらって更新してもらう方法です。

この「ユーザー操作が必須」という前提を受け入れたうえで、実装として重要になるのがFileProviderとインストール許可(不明なアプリのインストール)の扱いです。ここを押さえれば、社内配布でも“アプリ内から更新導線を作る”ことは十分可能です。

なぜサイレント更新できないのか(Android のセキュリティモデル)

Android は、アプリが勝手に他アプリ(=APK)をインストール・更新してしまうと、マルウェア的な振る舞いが可能になってしまいます。そのため、通常のアプリが OS に対して「黙って上書き更新して」と命令することは制限されています。

例外的に、以下のような“管理された環境”では自動インストールが成立することがありますが、これは一般アプリの範囲を超えます。

  • MDM / EMM(Intune など)で管理されている企業端末(管理者側が配布・更新を制御)
  • デバイスオーナー / プロファイルオーナーとして動作する管理アプリ(キッティング済み端末や専用端末)
  • 一部のメーカー独自機能、またはシステムアプリ相当の権限を持つ環境

「社内端末でも、ユーザーが何も触らずに強制更新したい」という要件がある場合は、後半で紹介するMDM 配布やManaged Google Play(組織内限定配布)の方が現実的です。

実装の全体像(やることを俯瞰する)

ストア外配布 APK の“アプリ内更新”は、ざっくり次の流れで作ります。

フェーズやることハマりどころ
更新確認サーバー/DB から最新バージョン情報を取得し、更新が必要か判定versionCode(Android の数値)で比較しないと事故りやすい
ダウンロードAPK をアプリのローカル領域(Cache など)へ保存保存場所が外部ストレージだと権限・Scoped Storage で面倒
検証SHA-256 などで改ざんチェック、署名やサイズ確認ダウンロード失敗時に「解析エラー」に見える
インストール導線FileProvider で content:// URI を作り、インストール画面を起動file:// を渡すと Android 7+ で例外、権限フラグ不足でも失敗
許可・案内「この提供元からのインストール」を許可してもらうAndroid 8+ はアプリ単位で許可。UI の案内が必須

APK をローカルに保存する(.NET MAUI での定番は CacheDirectory)

まず APK を端末へ落とし込みます。おすすめはアプリ専用領域です。とくに .NET MAUI では FileSystem.Current.CacheDirectory が扱いやすく、外部ストレージ権限を避けやすいです。

例として、HTTP で APK を取得してキャッシュに保存する最小コードは次の通りです(実運用では後述の改ざん検知も入れるのが安全です)。

using System.Net.Http;

public static class ApkDownloadService
{
    public static async Task<string> DownloadApkAsync(string apkUrl, CancellationToken ct = default)
    {
        // 保存先(例:キャッシュ)
        var apkPath = Path.Combine(FileSystem.Current.CacheDirectory, "app-update.apk");

        using var http = new HttpClient();
        using var response = await http.GetAsync(apkUrl, HttpCompletionOption.ResponseHeadersRead, ct);
        response.EnsureSuccessStatusCode();

        await using var input = await response.Content.ReadAsStreamAsync(ct);
        await using var output = File.Create(apkPath);
        await input.CopyToAsync(output, ct);

        return apkPath;
    }
}

保存先を CacheDirectory にするメリットは次の通りです。

  • 外部ストレージ権限を回避しやすい(Scoped Storage の影響を受けにくい)
  • アプリのアンインストールで自動的に消える(端末内に APK が残りにくい)
  • FileProvider の <cache-path> で素直に共有できる

逆に、外部ストレージ(Download フォルダなど)へ保存する設計は、権限や端末差異が増えるので、まずはアプリ専用領域で完結させる方がトラブルが少ないです。

FileProvider を追加する(ストア外 APK インストールの必須パーツ)

Android 7.0(API 24)以降、アプリ間でファイルパス(file://)を直接渡すと例外になるケースが増えました。APK のインストールに限らず、外部へファイルを渡すときはFileProvider で content:// URI に変換して渡すのが定石です。

必要なファイルと配置場所(MAUI の標準構成)

.NET MAUI の Android 側は、一般的に以下の場所へファイルを置きます。

項目配置場所(例)役割
AndroidManifest.xmlPlatforms/Android/AndroidManifest.xmlFileProvider 定義・権限宣言
provider_paths.xmlPlatforms/Android/Resources/xml/provider_paths.xmlFileProvider が共有してよいパスの定義
インストール起動コード#if ANDROID ブロック、または Platforms/Android 実装Intent 生成と StartActivity

AndroidManifest.xml に FileProvider を定義する

<application> 配下に FileProvider を追加します。authority はパッケージ名 + 固定文字列にしておくと衝突しにくいです。ここでは ${applicationId}.fileprovider を採用します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  <!-- APK インストール許可(後述) -->
  <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

  <application>

    <provider
      android:name="androidx.core.content.FileProvider"
      android:authorities="${applicationId}.fileprovider"
      android:exported="false"
      android:grantUriPermissions="true">

      <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />

    </provider>

  </application>
</manifest>

よくあるミスは次の 2 つです。

  • authority がコード側と一致しない(インストール起動時に例外になる)
  • android:grantUriPermissions が true でない(読み取り許可が渡せず失敗する)

provider_paths.xml を作成する(CacheDirectory を共有する例)

次に、FileProvider が参照してよいパスを定義します。CacheDirectory を使うなら <cache-path> が最もシンプルです。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
  <!-- アプリの内部キャッシュ配下を共有対象にする -->
  <cache-path name="cache" path="." />
</paths>

APK をサブフォルダに入れる場合(例:CacheDirectory/apk/app-update.apk)は、path="apk/" のように限定しても構いません。共有範囲は最小化するほど安全です。

APK インストール関連の権限と「不明なアプリのインストール」許可

Manifest に android.permission.REQUEST_INSTALL_PACKAGES を宣言したとしても、端末側で「このアプリからのインストール」を許可しない限り、インストールはブロックされることがあります。特に Android 8.0(API 26)以降はアプリ単位で許可が必要です。

やってはいけない権限:INSTALL_PACKAGES

検索すると android.permission.INSTALL_PACKAGES が出てくることがありますが、これは多くの環境で一般アプリに許可されない(システム権限相当)です。社内配布であっても、普通の署名アプリでは使えない前提で設計した方が安全です。

許可が必要かをチェックし、設定画面へ誘導する

ユーザー体験としては、いきなりインストール画面を出して失敗させるより、事前に「許可が必要です」と案内して設定画面へ誘導した方がスムーズです。Android 8.0+ では、アプリ単位の許可状態を CanRequestPackageInstalls() でチェックできます。

#if ANDROID
using Android.Content;
using Android.OS;
using Android.Provider;

public static class UnknownSourcesHelper
{
    public static bool EnsureCanInstallPackages()
    {
        var context = Android.App.Application.Context;

        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            if (!context.PackageManager.CanRequestPackageInstalls())
            {
                // 「この提供元からのインストール」許可設定画面へ
                var intent = new Intent(Settings.ActionManageUnknownAppSources);
                intent.SetData(Android.Net.Uri.Parse($"package:{context.PackageName}"));
                intent.AddFlags(ActivityFlags.NewTask);
                context.StartActivity(intent);
                return false;
            }
        }

        return true;
    }
}
#endif

この関数を、APK ダウンロード後・インストール起動前のタイミングで呼び出し、false の場合は「設定後にもう一度押してください」と案内するのが現実的です。

Intent でインストール画面を起動する(FileProvider で content:// を渡す)

APK を保存し、FileProvider を設定したら、最後にやることは「インストール UI を起動する」だけです。ポイントは次の 3 つです。

  • FileProvider で content:// URI を作る
  • MIME タイプは application/vnd.android.package-archive
  • GrantReadUriPermission を付ける(相手が APK を読めるようにする)
#if ANDROID
using Android.Content;
using AndroidX.Core.Content;
using Java.IO;

public static class ApkInstaller
{
    public static void StartInstall(string apkFilePath)
    {
        var context = Android.App.Application.Context;

        // 不明ソース許可の確認(Android 8+)
        if (!UnknownSourcesHelper.EnsureCanInstallPackages())
        {
            // 設定へ誘導したので一旦終了(ユーザーが戻ってきたら再試行)
            return;
        }

        var apkFile = new File(apkFilePath);

        // Manifest の authorities と一致させる
        var authority = $"{context.PackageName}.fileprovider";

        var uri = FileProvider.GetUriForFile(context, authority, apkFile);

        var intent = new Intent(Intent.ActionView);
        intent.SetDataAndType(uri, "application/vnd.android.package-archive");
        intent.AddFlags(ActivityFlags.GrantReadUriPermission);
        intent.AddFlags(ActivityFlags.NewTask);

        context.StartActivity(intent);
    }
}
#endif

この実装で、ダウンロード済み APK のインストール(既存アプリの更新)画面が起動します。ユーザーが「インストール」や「更新」をタップすると、上書き更新が進みます。

インストール完了をアプリ側で“確実に”検知できる?

更新中はアプリが置き換えられるため、プロセスが再起動したり、直後の状態が不安定になりがちです。一般的には、インストール完了を完璧にトラッキングするよりも、次回起動時にバージョンを再チェックして「更新済み」を表示する方が堅牢です。

.NET MAUI で Android 固有コードをきれいに分離する方法

記事冒頭の通り、#if ANDROID で囲うのが手早い方法です。ただ、規模が大きくなると見通しが悪くなるので、実務ではインターフェース + プラットフォーム実装で分離しておくと運用が楽になります。

共有側:インターフェースを定義する

public interface IAppUpdateInstaller
{
    Task CanInstallAsync();
    Task StartInstallAsync(string apkFilePath);
}

Android 側:Platforms/Android に実装を置く

#if ANDROID
using Android.Content;
using Android.OS;
using Android.Provider;
using AndroidX.Core.Content;
using Java.IO;

public class AndroidAppUpdateInstaller : IAppUpdateInstaller
{
public Task CanInstallAsync()
{
var context = Android.App.Application.Context;


    if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
    {
        if (!context.PackageManager.CanRequestPackageInstalls())
        {
            var intent = new Intent(Settings.ActionManageUnknownAppSources);
            intent.SetData(Android.Net.Uri.Parse($"package:{context.PackageName}"));
            intent.AddFlags(ActivityFlags.NewTask);
            context.StartActivity(intent);
            return Task.FromResult(false);
        }
    }

    return Task.FromResult(true);
}

public Task StartInstallAsync(string apkFilePath)
{
    var context = Android.App.Application.Context;

    var file = new File(apkFilePath);
    var authority = $"{context.PackageName}.fileprovider";
    var uri = FileProvider.GetUriForFile(context, authority, file);

    var intent = new Intent(Intent.ActionView);
    intent.SetDataAndType(uri, "application/vnd.android.package-archive");
    intent.AddFlags(ActivityFlags.GrantReadUriPermission | ActivityFlags.NewTask);

    context.StartActivity(intent);
    return Task.CompletedTask;
}


}
#endif

DI 登録して共有コードから呼び出す(例)

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

#if ANDROID
builder.Services.AddSingleton();
#endif


    return builder.Build();
}


}

共有側の更新フロー(ダウンロード → インストール誘導)は、IAppUpdateInstaller に対して呼び出すだけになるので、保守しやすくなります。

更新処理の実務的な流れ(おすすめの UI / UX)

社内配布の APK 更新は「技術的にインストール画面を出せる」だけでは不十分で、実際の現場ではユーザーが迷わず更新できる導線が重要です。以下の流れがトラブルになりにくいです。

ステップユーザーに見せる内容実装ポイント
1. 更新通知「新しいバージョンがあります」「更新内容」リリースノートをサーバー側で管理すると運用が楽
2. ダウンロード進捗、通信量注意、Wi-Fi 推奨失敗時の再試行、タイムアウト、キャンセル対応
3. 許可の案内「このアプリからのインストールを許可してください」Android 8+ の設定画面へ遷移し、戻ったら再試行
4. インストール開始インストール画面へ遷移することを説明FileProvider + Intent。ここで初めて OS UI を起動
5. 完了確認次回起動時に「更新完了」表示起動時バージョンチェックで判定する方が堅牢

改ざん対策と事故防止(社内配布でも必須)

ストア外配布 APK は、配布経路が社内だとしても「配布物=実行コード」です。更新機構をアプリ内に入れるなら、最低限次は押さえておくと安心です。

ダウンロードした APK の SHA-256 を検証する

配布サーバー側で APK の SHA-256 を保持し、端末側で一致確認するだけでも、通信途中の破損や意図しない差し替え事故に強くなります。

using System.Security.Cryptography;

public static class HashUtil
{
    public static async Task<string> Sha256Async(string filePath, CancellationToken ct = default)
    {
        await using var stream = File.OpenRead(filePath);
        using var sha = SHA256.Create();
        var hash = await sha.ComputeHashAsync(stream, ct);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }
}

実務では、サーバー側で返す JSON に apkUrl と一緒に sha256 を含め、ダウンロード後に一致してからインストールへ進めるのが安全です。

署名(keystore)が違うと更新できない

Android の上書き更新は、基本的に同じ署名で署名された APK でなければ更新できません。社内配布でよく起きるのが、ビルドマシンや担当者変更で keystore が変わり、突然アップデートできなくなる事故です。

運用ルールとして、次を徹底するとトラブルが激減します。

  • 本番用の keystore は厳格に管理し、更新担当が変わっても同じ署名で出せる体制にする
  • CI/CD(ビルドパイプライン)で署名し、手元ビルド APK を配布しない
  • バージョンコード(versionCode)は単調増加にする(戻すとインストール拒否される)

よくあるエラーと対処(社内配布の現場で詰まりやすい)

ストア外配布 APK の“アプリ内更新”で詰まりやすいポイントを、症状別にまとめます。

症状原因として多いもの対処
インストール画面が開かない/例外が出るauthority 不一致、provider_paths 未配置、FileProvider 未定義Manifest の authorities とコードの authority を一致させ、provider_paths.xml の位置を確認
「このファイルを開けません」「権限がありません」GrantReadUriPermission フラグ不足ActivityFlags.GrantReadUriPermission を追加
「インストールがブロックされました」不明なアプリのインストール許可が OFFAndroid 8+ の設定画面へ誘導し、許可後に再実行
「アプリがインストールされませんでした」署名が違う、versionCode が小さい、パッケージ名が違う同一 keystore で署名、versionCode を増やす、パッケージ名を同一にする
「解析エラー(パッケージを解析できません)」APK 破損、途中で途切れた、対象 CPU/SDK 不一致サイズ・ハッシュ検証、再ダウンロード、ABI/SDK のビルド設定確認

ストア外配布で“更新”に見せるためのポイント

ユーザーから見ると、Play ストア更新と同じ感覚を期待しがちです。ストア外配布では体験が違うため、アプリ側の説明で不安を減らす工夫が重要です。

  • 更新ボタンの前に注意書き:「インストール画面が開きます」「完了後にアプリを再起動してください」など
  • 権限許可の説明をスクリーンショット付きで用意(社内 Wiki でも可)
  • 更新対象の判定を厳密に:バージョン名だけでなく、内部の versionCode 相当を採用
  • 失敗時のリカバリ導線:再ダウンロード、サポート連絡、端末再起動など

サイレント更新が本当に必要なら:一般アプリの範囲を超えた選択肢

「ユーザーが触らなくても強制更新したい」「起動したら必ず最新にしたい」という要件は、ストア外配布 APK をアプリだけで完結させるのが難しい領域です。その場合は、最初から配布の仕組みを変える方が、長期的にコストと事故を減らせます。

選択肢向いているケース特徴
MDM / EMM(Intune など)端末を企業が管理できる(社給端末、キオスク端末)管理者側で配布・更新を制御しやすい。ユーザー操作を最小化できる
Managed Google Play(組織内限定配布)Google Play を「社内限定」で使える組織Play の仕組みで更新が回る。In-app update などの導線も取りやすい
デバイスオーナー/専用管理アプリ専用端末・現場端末で強い制御が必要運用難度は上がるが、最も自動化しやすい

“とりあえずアプリだけで更新したい”段階では、本記事の FileProvider + インストール画面起動が最短です。一方で、更新頻度が高い・対象台数が多い・夜間に自動更新したいといった状況では、早めに MDM を検討した方が総コストが下がることが多いです。

まとめ:.NET MAUI で社内配布 APK を更新する最短ルート

ストア外配布の APK を .NET MAUI アプリ内から更新したい場合、実装の核心は次の通りです。

  • APK を FileSystem.Current.CacheDirectory などのローカルへ保存する
  • FileProvider を Manifest に追加し、provider_paths.xml を用意する
  • REQUEST_INSTALL_PACKAGES を宣言し、Android 8+ では許可画面へ誘導できるようにする
  • Intent.ActionView でインストール画面を起動し、ユーザーに更新してもらう
  • 改ざん・破損対策として、ハッシュ検証や署名運用を整える

これらを押さえれば、Google Play に公開していない社内配布アプリでも、アプリ内に“更新ボタン”を用意し、ユーザーが迷わず最新版へ移行できる導線を作れます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次