📱

UnrealEngineのLaunchURLがAndroidで動作しないことがある問題とその解決

2023/08/23に公開

はじめに

この記事ではUnreal Engine側のコードを修正して解決していますが、UEのコードビルドは必要なくEpicランチャー版でも修正可能です。
修正は自己責任でお願いします。

問題の内容

AndroidではSDK30で端末にある他のパッケージを見る権限の制限が強くなりました。その結果、正しい実装をしないとgetPackageManager()が想定通り動作しないことがあります。

https://developer.android.com/training/package-visibility/use-cases?hl=ja#open-urls-browser-or-other-app

https://stackoverflow.com/questions/62535856/intent-resolveactivity-returns-null-in-api-30

その影響を受けてか、UnrealEngineのAndroidビルドで以下のような現象があることを確認しました。

  • ブループリントでLaunchURLを実行したとき、引数のURLを処理できるアプリが端末内にない場合はブラウザが開くが、逆の場合は何も起きない
    • Twitter、Lineなど特定のURLを処理できるアプリをインストール済みの端末で、特定のURL(TwitterのURLやLineURLスキーム)をLaunchURLで実行しても何も起きない
    • Twitter、Lineなど特定のURLを処理できるアプリをアンインストールして同じURLを実行するとブラウザが開く

この問題はフォーラムに上がっていて、UE4.27で最低限のSDK30対応がされているようですが、それが不十分なようです。

https://forums.unrealengine.com/t/solved-launch-url-doesnt-work-in-android-for-sdk-target-30/488399/4

※フォーラムでリンクされているGithubIssueはUEのリポジトリ閲覧権限(無料)を持っていないと参照できません。閲覧権限の申請方法はこちら

またフォーラムで触れられている通りQUERY_ALL_PACKAGESの権限をManifestに追加するのは完全に悪手です。追加の権限申告フォームへの記入が求められますし、ポリシーを満たさない場合や申告せずに使っている場合はマーケットからアプリが削除される可能性があります。

https://support.google.com/googleplay/android-developer/answer/10158779

問題の原因

LaunchURLの実行はUnrealEngineのAndroid用Javaコードに移譲されており、その中でURLを処理できるActivityがあるかどうかの判断しています。
その中のif分岐に問題があります。

Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java.template
public void AndroidThunkJava_LaunchURL(String URL)
  {
    Log.debug("[JAVA} AndroidThunkJava_LaunchURL: URL = " + URL);
    if (!URL.contains("://"))
    {
      // add http:// if there isn't a scheme before a colon
      if (URL.indexOf(":") < 1)
      {
        URL = "http://" + URL;
        Log.debug("[JAVA} AndroidThunkJava_LaunchURL: corrected URL = " + URL);
      }
    }
    try
    {
      Intent BrowserIntent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(URL));
      BrowserIntent.addCategory(Intent.CATEGORY_BROWSABLE);

      // open browser on its own task
      BrowserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
      BrowserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);

      // make sure there is a web browser to handle the URL before trying to start activity (or may crash!)
      if (BrowserIntent.resolveActivity(getPackageManager()) != null)
      {
        Log.debug("[JAVA} AndroidThunkJava_LaunchURL: Starting activity");
        startActivity(BrowserIntent);
      }
      else
      {
        Log.debug("[JAVA} AndroidThunkJava_LaunchURL: Could not find an application to receive the URL intent");
      }
    }
    catch (Exception e)
    {
      Log.debug("[JAVA} AndroidThunkJava_LaunchURL: Failed with exception " + e.getMessage());
    }
  }

if (BrowserIntent.resolveActivity(getPackageManager()) != null)の部分が問題で、URLが処理できるアプリがあるとBrowserIntent.resolveActivity(getPackageManager())がNullで返ってきます。このせいでstartActivity(BrowserIntent)が発行されずアプリ上では何も起きません。ブラウザ以外で引数のURLを処理できるアプリがあるとNullで、ないとNotNullなのは不自然な動きにしか思えませんが……もはやgetPackageManager()を使って処理できるパッケージがあるかどうかを判断するのは時代遅れなのかもしれません。

解決策

そもそもAndroidの公式ではどのようなコードでURLのintentを発行しているかを見てみるのが良さそうです。

try {
    val intent = Intent(ACTION_VIEW, Uri.parse(url)).apply {
        // The URL should either launch directly in a non-browser app (if it's
        // the default), or in the disambiguation dialog.
        addCategory(CATEGORY_BROWSABLE)
        flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER
    }
    startActivity(intent)
} catch (e: ActivityNotFoundException) {
    // Only browser apps are available, or a browser is the default.
    // So you can open the URL directly in your app, for example in a
    // Custom Tab.
    openInCustomTabs(url)
}

https://developer.android.com/training/package-visibility/use-cases#let-non-browser-apps-handle-urls

こんな感じでstartActivity(intent)でintentをぶん投げちゃって、try-catchで握り潰してます。公式ではcatchの中でCustomTabを開くようになってますが、UEのAndroidビルドにCustomTabを入れるのは難儀なのでなくても良いでしょう。ブラウザアプリがひとつでもあればcatchには入らないですしね。

そして実はUEのコードの問題のif文の前に// make sure there is a web browser to handle the URL before trying to start activity (or may crash!)と書いてますが、そもそもtry-catchしてるのでクラッシュはしません。resolveActivity()を使ったif文が余計で、これがなくてもなんら問題ないです。

つまり、この問題の解決方法はBrowserIntent.resolveActivity(getPackageManager()) を使ってるif分岐を外して確実に startActivity(BrowserIntent) を実行することです。

Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java.template
public void AndroidThunkJava_LaunchURL(String URL)
  {
    Log.debug("[JAVA} AndroidThunkJava_LaunchURL: URL = " + URL);
    if (!URL.contains("://"))
    {
      // add http:// if there isn't a scheme before a colon
      if (URL.indexOf(":") < 1)
      {
        URL = "http://" + URL;
        Log.debug("[JAVA} AndroidThunkJava_LaunchURL: corrected URL = " + URL);
      }
    }
    try
    {
      Intent BrowserIntent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(URL));
      BrowserIntent.addCategory(Intent.CATEGORY_BROWSABLE);

      // open browser on its own task
      BrowserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
      BrowserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);

      // make sure there is a web browser to handle the URL before trying to start activity (or may crash!)
      //if (BrowserIntent.resolveActivity(getPackageManager()) != null)
      //{
        Log.debug("[JAVA} AndroidThunkJava_LaunchURL: Starting activity");
        startActivity(BrowserIntent);
      //}
      //else
      //{
      //  Log.debug("[JAVA} AndroidThunkJava_LaunchURL: Could not find an application to receive the URL intent");
      //}
    }
    catch (Exception e)
    {
      Log.debug("[JAVA} AndroidThunkJava_LaunchURL: Failed with exception " + e.getMessage());
    }
  }

この修正を入れたUEでAndroidビルドすれば、引数のURLを拾えるActivityがいい感じに起動してくれるようになります。めでたしめでたし。

GitHubで編集を提案
PY

Discussion