251 lines
11 KiB
C#
251 lines
11 KiB
C#
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("<UniWebView> Auth callback url contains an empty scheme. Please check the url: " + url);
|
|
return null;
|
|
}
|
|
|
|
var filter = CreateElement("intent-filter");
|
|
|
|
// Check if the scheme is HTTPS
|
|
// For Android App Links, we need to set the autoVerify attribute to true
|
|
// This allows Android to automatically verify the association between the app and the website
|
|
// Note: Only applies to HTTPS URLs, although Android App Links also support HTTP, we do not want it in
|
|
// UniWebView.
|
|
// https://developer.android.com/training/app-links
|
|
if (scheme == "https") {
|
|
filter.SetAttribute("autoVerify", AndroidXmlNamespace, "true");
|
|
}
|
|
|
|
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) {
|
|
// For HTTPS, the default port is 443. We only need to set the port explicitly
|
|
// if we're not using HTTPS or if we're using a non-standard HTTPS port.
|
|
if (scheme != "https" || uri.Port != 443) {
|
|
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;
|
|
}
|
|
} |