PostbuildModifyAndroidManifest.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. using System;
  2. using System.IO;
  3. using System.Text.RegularExpressions;
  4. using UnityEditor.Android;
  5. using UnityEditor.Build;
  6. namespace SingularityGroup.HotReload.Editor {
  7. #pragma warning disable CS0618
  8. /// <remarks>
  9. /// <para>
  10. /// This class sets option in the AndroidManifest that you choose in HotReload build settings.
  11. /// </para>
  12. /// <para>
  13. /// - To connect to the HotReload server through the local network, we need to permit access to http://192...<br/>
  14. /// - Starting with Android 9, insecure http requests are not allowed by default and must be whitelisted
  15. /// </para>
  16. /// </remarks>
  17. internal class PostbuildModifyAndroidManifest : IPostGenerateGradleAndroidProject {
  18. #pragma warning restore CS0618
  19. public int callbackOrder => 10;
  20. private const string manifestFileName = "AndroidManifest.xml";
  21. public void OnPostGenerateGradleAndroidProject(string path) {
  22. try {
  23. if (!HotReloadBuildHelper.IncludeInThisBuild()) {
  24. return;
  25. }
  26. // Note: in future we may support users with custom configuration for usesCleartextTraffic
  27. #if UNITY_2022_1_OR_NEWER
  28. // Unity 2022 or newer → do nothing, we rely on Unity option to control the flag
  29. #else
  30. // Unity 2021 or older → put manifest flag in if Unity is making a Development Build
  31. var manifestFilePath = FindAndroidManifest(path);
  32. if (manifestFilePath == null) {
  33. throw new BuildFailedException($"[{CodePatcher.TAG}] Unable to find {manifestFileName}");
  34. }
  35. SetUsesCleartextTraffic(manifestFilePath);
  36. #endif
  37. } catch (BuildFailedException) {
  38. throw;
  39. } catch (Exception e) {
  40. throw new BuildFailedException(e);
  41. }
  42. }
  43. /// identifier that is used in the deeplink uri scheme
  44. /// (initially tried Application.identifier, but that was giving unexpected results based on PlayerSettings)
  45. // SG-29580
  46. // Something to uniqly identify the application, but it must be something which is highly likely
  47. // to be the same at build time (studio might have logic to set e.g. product name to MyGameProd or MyGameTest)
  48. public static string ApplicationIdentiferSlug => "app";
  49. /*
  50. public static string ApplicationIdentiferSlug => Regex.Replace(ApplicationIdentifer, @"[^a-zA-Z0-9\.\-]", "")
  51. .Replace("..", ".") // happens if your companyname in Unity ends with a dot
  52. .ToLowerInvariant();
  53. private static void AddDeeplinkForwarder(string manifestFilePath) {
  54. // add the hotreload-${identifier} uri scheme to the AndroidManifest.xml file
  55. // it should be added as part of an intent-filter for the activity "com.singularitygroup.deeplinkforwarder.DeepLinkForwarderActivity"
  56. var contents = File.ReadAllText(manifestFilePath);
  57. if (contents.Contains("android:name=\"com.singularitygroup.deeplinkforwarder.DeepLinkForwarderActivity\"")) {
  58. // user has already set this themselves, don't replace it
  59. return;
  60. }
  61. //note: not using android:host or any other data attr because android still shows a chooser for all ur hotreload apps
  62. // Therefore must use a unique uri scheme to ensure only one app can handle it.
  63. var activityWithIntentFilter = @"
  64. <activity android:name=""com.singularitygroup.deeplinkforwarder.DeepLinkForwarderActivity"">
  65. <intent-filter>
  66. <action android:name=""android.intent.action.VIEW"" />
  67. <category android:name=""android.intent.category.DEFAULT"" />
  68. <category android:name=""android.intent.category.BROWSABLE"" />
  69. <data android:scheme=""hotreload-" + ApplicationIdentiferSlug + @""" />
  70. </intent-filter>
  71. </activity>";
  72. var newContents = Regex.Replace(contents,
  73. @"</application>",
  74. activityWithIntentFilter + "\n </application>"
  75. );
  76. File.WriteAllText(manifestFilePath, newContents);
  77. }
  78. */
  79. // Assume unityLibraryPath is to {gradleProject}/unityLibrary/ which is roughly the same across Unity versions 2018/2019/2020/2021/2022
  80. private static string FindAndroidManifest(string unityLibraryPath) {
  81. // find the AndroidManifest.xml file which we can edit
  82. var dir = new DirectoryInfo(unityLibraryPath);
  83. var manifestFilePath = Path.Combine(dir.FullName, "src", "main", manifestFileName);
  84. if (File.Exists(manifestFilePath)) {
  85. return manifestFilePath;
  86. }
  87. Log.Info("Did not find {0} at {1}, searching for manifest file inside {2}", manifestFileName, manifestFilePath, dir.FullName);
  88. var manifestFiles = dir.GetFiles(manifestFileName, SearchOption.AllDirectories);
  89. if (manifestFiles.Length == 0) {
  90. return null;
  91. }
  92. foreach (var file in manifestFiles) {
  93. if (file.FullName.Contains("src")) {
  94. // good choice
  95. return file.FullName;
  96. }
  97. }
  98. // fallback to the first file found
  99. return manifestFiles[0].FullName;
  100. }
  101. /// <summary>
  102. /// Set option android:usesCleartextTraffic="true"
  103. /// </summary>
  104. /// <param name="manifestFilePath">Absolute filepath to the unityLibrary AndroidManifest.xml file</param>
  105. private static void SetUsesCleartextTraffic(string manifestFilePath) {
  106. // Ideally we would create or modify a "Network Security Configuration file" to permit access to local ip addresses
  107. // https://developer.android.com/training/articles/security-config#manifest
  108. // but that becomes difficult when the user has their own configuration file - would need to search for it and it may be inside an aar.
  109. var contents = File.ReadAllText(manifestFilePath);
  110. if (contents.Contains("android:usesCleartextTraffic=")) {
  111. // user has already set this themselves, don't replace it
  112. return;
  113. }
  114. var newContents = Regex.Replace(contents,
  115. @"<application\s",
  116. "<application android:usesCleartextTraffic=\"true\" "
  117. );
  118. newContents += $"\n<!-- [{CodePatcher.TAG}] Added android:usesCleartextTraffic=\"true\" to permit connecting to the Hot Reload http server running on your machine. -->";
  119. newContents += $"\n<!-- [{CodePatcher.TAG}] This change only happens in Unity development builds. You can disable this in the Hot Reload settings window. -->";
  120. File.WriteAllText(manifestFilePath, newContents);
  121. }
  122. }
  123. }