using System.Xml; using System.Text; using System; using UnityEngine; internal class UniWebViewAndroidXmlDocument : XmlDocument { private readonly string path; protected readonly XmlNamespaceManager nameSpaceManager; protected const string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; protected UniWebViewAndroidXmlDocument(string path) { this.path = path; using (var reader = new XmlTextReader(path)) { reader.Read(); Load(reader); } nameSpaceManager = new XmlNamespaceManager(NameTable); nameSpaceManager.AddNamespace("android", AndroidXmlNamespace); } public void Save() { SaveAs(path); } private void SaveAs(string path) { using var writer = new XmlTextWriter(path, new UTF8Encoding(false)); writer.Formatting = Formatting.Indented; Save(writer); } } internal class UniWebViewAndroidManifest : UniWebViewAndroidXmlDocument { private readonly XmlElement manifestElement; private readonly XmlElement applicationElement; public UniWebViewAndroidManifest(string path) : base(path) { manifestElement = SelectSingleNode("/manifest") as XmlElement; applicationElement = SelectSingleNode("/manifest/application") as XmlElement; } private XmlAttribute CreateAndroidAttribute(string key, string value) { XmlAttribute attr = CreateAttribute("android", key, AndroidXmlNamespace); attr.Value = value; return attr; } internal XmlNode GetActivityWithLaunchIntent() { return SelectSingleNode( "/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and " + "intent-filter/category/@android:name='android.intent.category.LAUNCHER']", nameSpaceManager); } internal bool SetUsesCleartextTraffic() { var changed = false; if (applicationElement.GetAttribute("usesCleartextTraffic", AndroidXmlNamespace) != "true") { applicationElement.SetAttribute("usesCleartextTraffic", AndroidXmlNamespace, "true"); changed = true; } return changed; } internal bool SetHardwareAccelerated() { var changed = false; var activity = GetActivityWithLaunchIntent() as XmlElement; if (activity == null) { Debug.LogError( "There is no launch intent activity in the AndroidManifest.xml." + " Please check your AndroidManifest.xml file and make sure it has a main activity with intent filter"); return false; } if (activity.GetAttribute("hardwareAccelerated", AndroidXmlNamespace) != "true") { activity.SetAttribute("hardwareAccelerated", AndroidXmlNamespace, "true"); changed = true; } return changed; } internal bool AddCameraPermission() { var changed = false; var cameraPermission = "/manifest/uses-permission[@android:name='android.permission.CAMERA']"; var cameraPermissionNode = SelectNodes(cameraPermission, nameSpaceManager); if (cameraPermissionNode == null || cameraPermissionNode.Count == 0) { var elem = CreateElement("uses-permission"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.CAMERA")); manifestElement.AppendChild(elem); changed = true; } var hardwareCamera = "/manifest/uses-feature[@android:name='android.hardware.camera']"; var hardwareCameraNode = SelectNodes(hardwareCamera, nameSpaceManager); if (hardwareCameraNode == null || hardwareCameraNode.Count == 0) { var elem = CreateElement("uses-feature"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.camera")); manifestElement.AppendChild(elem); changed = true; } return changed; } internal bool AddMicrophonePermission() { bool changed = false; var microphonePermission = "/manifest/uses-permission[@android:name='android.permission.MICROPHONE']"; var microphonePermissionNode = SelectNodes(microphonePermission, nameSpaceManager); if (microphonePermissionNode == null || microphonePermissionNode.Count == 0) { var elem = CreateElement("uses-permission"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.MICROPHONE")); manifestElement.AppendChild(elem); changed = true; } var microphoneHardware = "/manifest/uses-feature[@android:name='android.hardware.microphone']"; var microphoneHardwareNode = SelectNodes(microphoneHardware, nameSpaceManager); if (microphoneHardwareNode == null || microphoneHardwareNode.Count == 0) { var elem = CreateElement("uses-feature"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.microphone")); manifestElement.AppendChild(elem); changed = true; } return changed; } internal bool AddReadExternalStoragePermission() { var changed = false; var externalPermission = "/manifest/uses-permission[@android:name='android.permission.READ_EXTERNAL_STORAGE']"; var externalNode = SelectNodes(externalPermission, nameSpaceManager); if (externalNode == null || externalNode.Count == 0) { var elem = CreateElement("uses-permission"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_EXTERNAL_STORAGE")); manifestElement.AppendChild(elem); changed = true; } return changed; } internal bool AddWriteExternalStoragePermission() { var changed = false; var externalPermission = "/manifest/uses-permission[@android:name='android.permission.WRITE_EXTERNAL_STORAGE']"; var externalNode = SelectNodes(externalPermission, nameSpaceManager); if (externalNode == null || externalNode.Count == 0) { var elem = CreateElement("uses-permission"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.WRITE_EXTERNAL_STORAGE")); manifestElement.AppendChild(elem); changed = true; } return changed; } internal bool AddAccessFineLocationPermission() { var changed = false; var locationPermission = "/manifest/uses-permission[@android:name='android.permission.ACCESS_FINE_LOCATION']"; var locationNode = SelectNodes(locationPermission, nameSpaceManager); if (locationNode == null || locationNode.Count == 0) { var elem = CreateElement("uses-permission"); elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.ACCESS_FINE_LOCATION")); manifestElement.AppendChild(elem); changed = true; } return changed; } internal bool AddAuthCallbacksIntentFilter(string[] authCallbackUrls) { var changed = false; XmlElement authActivityNode; if (authCallbackUrls.Length > 0) { var authActivity = "/manifest/application/activity[@android:name='com.onevcat.uniwebview.UniWebViewAuthenticationActivity']"; var list = SelectNodes(authActivity, nameSpaceManager); if (list == null || list.Count == 0) { var created = CreateElement("activity"); created.SetAttribute("name", AndroidXmlNamespace, "com.onevcat.uniwebview.UniWebViewAuthenticationActivity"); created.SetAttribute("exported", AndroidXmlNamespace, "true"); created.SetAttribute("launchMode", AndroidXmlNamespace, "singleTask"); created.SetAttribute("configChanges", AndroidXmlNamespace, "orientation|screenSize|keyboardHidden"); authActivityNode = created; } else { authActivityNode = list[0] as XmlElement; } } else { return false; } foreach (var url in authCallbackUrls) { var intentFilter = CreateIntentFilter(url); if (intentFilter != null) { authActivityNode?.AppendChild(intentFilter); changed = true; } } if (authActivityNode != null) { applicationElement.AppendChild(authActivityNode); } return changed; } private XmlElement CreateIntentFilter(string url) { var uri = new Uri(url); var scheme = uri.Scheme; if (string.IsNullOrEmpty(scheme)) { Debug.LogError(" Auth callback url contains an empty scheme. Please check the url: " + url); return null; } var filter = CreateElement("intent-filter"); var action = CreateElement("action"); action.SetAttribute("name", AndroidXmlNamespace, "android.intent.action.VIEW"); filter.AppendChild(action); var defaultCategory = CreateElement("category"); defaultCategory.SetAttribute("name", AndroidXmlNamespace, "android.intent.category.DEFAULT"); filter.AppendChild(defaultCategory); var browseCategory = CreateElement("category"); browseCategory.SetAttribute("name", AndroidXmlNamespace, "android.intent.category.BROWSABLE"); filter.AppendChild(browseCategory); var data = CreateElement("data"); data.SetAttribute("scheme", AndroidXmlNamespace, scheme); if (!String.IsNullOrEmpty(uri.Host)) { data.SetAttribute("host", AndroidXmlNamespace, uri.Host); } if (uri.Port != -1) { data.SetAttribute("port", AndroidXmlNamespace, uri.Port.ToString()); } if (!string.IsNullOrEmpty(uri.PathAndQuery) && uri.PathAndQuery != "/") { data.SetAttribute("path", AndroidXmlNamespace, uri.PathAndQuery); } filter.AppendChild(data); return filter; } }