I am working on an application that is set as a system app on Android 10 inside a set top box, the application is given system privileges and originally the Rom that is running on the STB was using Android.System.WebView version 74.X. We now want to run new applications on this STB which use React, but the version of webview does not support javascript. So we upgraded to 114.X version of the webview. Now the react application works flawlessly, however the original system app which has the ability to show html pages and pdf pages inside our app no longer works. It displays a white screen always.
I spent time and removed all custom controls on the custom webview inside the app, but no errors are being thrown. I removed all references of reflection which was used previously and the webview just will not work inside the system app.
I then used the exact same application but sideloaded it as a release app but not system, the only difference it does not have the elevated privileges and it works as expected. I have the exact same AndroidManifest for the system app and Release app. Only difference is the use of android:sharedUserId="android.uid.system"
I have read that the reason it is being blocked is because of the security concern of a system app being made vulnerable when opening webviews.
Is there something to do to get this working as I have tried everything I can find online, including removing all custom controls and just displaying a basic google page.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="APPNAME.app" android:sharedUserId="android.uid.system" android:versionCode="42" android:versionName="2024.6.21.10" android:installLocation="auto">
<uses-sdk android:minSdkVersion="22" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
<uses-permission android:name="android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.HDMI_CEC" />
<uses-permission android:name="APPNAME.app.CLIENT_ADMIN_COMMUNICATION" />
<application android:label="APPNAME" android:icon="@drawable/ic_launcher" android:usesCleartextTraffic="true" />
</manifest>
using System.ComponentModel;
using Android.Content;
using Android.Views;
using Android.Views.InputMethods;
using Android.Webkit;
REMOVING CUSTOM USING TO HIDE APP NAME
using Serilog;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using WebView = Xamarin.Forms.WebView;
[assembly: ExportRenderer(typeof(HtmlView), typeof(HtmlViewRenderer))]
namespace APP.Renderers
{
public class HtmlViewRenderer : WebViewRenderer
{
private const string JsBridgeName = "jsBridge";
private HtmlWebViewClient _htmlWebViewClient;
private IInputMethodManagerWrapper _inputMethodManagerWrapper;
private IWindowWrapper _windowWrapper;
public HtmlViewRenderer(Context context)
: base(context)
{
var logger = Log.ForContext<HtmlViewRenderer>();
//WebViewHookHelper.HookWebView(logger);
PropertiesResolverHelper.ResolveProperties(this);
}
[Inject]
public IApiGatewayHttpClientProvider ApiGatewayHttpClientProvider { get; set; }
[Inject]
public IClientSettings ClientSettings { get; set; }
public IWebViewController ElementController => Element;
public HtmlView HtmlView => Element as HtmlView;
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
// this prevents loading page in base.OnElementChanged(e) until custom HtmlWebViewClient getting set
var source = Element.Source;
Element.Source = null;
base.OnElementChanged(e);
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface(JsBridgeName);
var hybridJsWebView = e.OldElement as HtmlView;
hybridJsWebView?.Cleanup();
}
if (e.OldElement == null)
{
_htmlWebViewClient = new HtmlWebViewClient(this);
Control.Settings.JavaScriptEnabled = true;
Control.Settings.DomStorageEnabled = true;
Control.Settings.AllowFileAccess = true;
Control.Settings.AllowContentAccess = true;
_htmlWebViewClient.OnLoadError += HtmlWebViewClientOnLoadError;
_htmlWebViewClient.OnHttpError += HtmlWebViewOnHttpError;
Control.ClearCache(true);
}
if (Control != null)
{
Control.Settings.JavaScriptEnabled = true;
Control.Settings.DomStorageEnabled = true;
Control.Settings.AllowFileAccess = true;
Control.Settings.AllowContentAccess = true;
Control.SetWebViewClient(_htmlWebViewClient);
Control.SetWebChromeClient(new WebChromeClient());
Control.LongClickable = false;
Control.SetOnLongClickListener(new DisabledLongClickListener());
Control.SetBackgroundColor(HtmlView.BackgroundColor.ToAndroid());
}
if (e.NewElement != null)
{
Control?.AddJavascriptInterface(new JsBridge(this), JsBridgeName);
if (e.NewElement is HtmlView htmlView)
{
_inputMethodManagerWrapper = htmlView.InputMethodManagerWrapper;
_windowWrapper = htmlView.WindowWrapper;
Control.Settings.JavaScriptEnabled = true;
Control.Settings.DomStorageEnabled = true;
}
}
source?.Load(this);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (HtmlView == null)
{
return;
}
if (e.PropertyName == nameof(HtmlView.DesktopMode) && HtmlView.DesktopMode && Control.Settings != null)
{
Control.Settings.UserAgentString = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1";
Control.Settings.JavaScriptEnabled = true;
Control.Settings.DomStorageEnabled = true;
Control.Settings.LoadWithOverviewMode = true;
Control.Settings.AllowFileAccess = true;
Control.Settings.AllowContentAccess = true;
Control.Settings.UseWideViewPort = true;
Control.Settings.SetSupportZoom(true);
Control.Settings.BuiltInZoomControls = true;
Control.Settings.DisplayZoomControls = true;
}
if (e.PropertyName == nameof(HtmlView.BackgroundColor))
{
Control.SetBackgroundColor(HtmlView.BackgroundColor.ToAndroid());
}
if (e.PropertyName == nameof(HtmlView.ForceKeyboardVisible))
{
if (HtmlView.ForceKeyboardVisible)
{
ShowKeyboard();
}
else
{
HideKeyboard();
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing && Control != null)
{
Control.LoadUrl("about:blank");
Control.ClearCache(true);
Control.Destroy();
_htmlWebViewClient.OnLoadError -= HtmlWebViewClientOnLoadError;
_htmlWebViewClient.OnHttpError -= HtmlWebViewOnHttpError;
_htmlWebViewClient?.Dispose();
if (HtmlView.ForceKeyboardVisible)
{
HideKeyboard();
}
}
base.Dispose(disposing);
}
private void HtmlWebViewClientOnLoadError(object sender, WebResourceError e)
{
HtmlView.HasError = true;
}
private void HtmlWebViewOnHttpError(object sender, WebResourceResponse e)
{
HtmlView.HasError = true;
}
private void ShowKeyboard()
{
if (Control != null)
{
_inputMethodManagerWrapper?.ShowSoftInput(Control, (int)ShowFlags.Forced);
}
}
private void HideKeyboard()
{
if (Control != null)
{
_inputMethodManagerWrapper?.HideSoftInputFromWindow(Control.WindowToken, (int)HideSoftInputFlags.None);
}
_windowWrapper?.SetSoftInputMode((int)SoftInput.StateHidden);
}
}
}
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using Android.Graphics;
using Android.Net.Http;
using Android.Runtime;
using Android.Webkit;
using Serilog;
using Serilog.Core;
using Xamarin.Forms;
using WebView = Android.Webkit.WebView;
namespace APPNAME.Controls
{
internal class HtmlWebViewClient : Android.Webkit.WebViewClient
{
private readonly HtmlViewRenderer _htmlViewRenderer;
public HtmlWebViewClient(HtmlViewRenderer htmlViewRenderer)
{
_htmlViewRenderer = htmlViewRenderer;
}
protected HtmlWebViewClient(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public event EventHandler<WebResourceError> OnLoadError;
public event EventHandler<WebResourceResponse> OnHttpError;
public override WebResourceResponse ShouldInterceptRequest(WebView view, IWebResourceRequest request)
{
if (_htmlViewRenderer != null && request.Url != null && _htmlViewRenderer.HtmlView.IsAuthenticationEnabled && request.Url.Port == _htmlViewRenderer.ClientSettings.ApiGatewayPort)
{
return AuthenticateApiGatewayRequest(request.Url.ToString(), request.Method, request.RequestHeaders, () => base.ShouldInterceptRequest(view, request));
}
return base.ShouldInterceptRequest(view, request);
}
public override void OnReceivedError(WebView view, IWebResourceRequest request, WebResourceError error)
{
base.OnReceivedError(view, request, error);
OnLoadError?.Invoke(this, error);
}
public override void OnReceivedHttpError(WebView view, IWebResourceRequest request, WebResourceResponse errorResponse)
{
base.OnReceivedHttpError(view, request, errorResponse);
if (request.Url != null && view.Url == request.Url.ToString())
{
OnHttpError?.Invoke(this, errorResponse);
}
}
public override void OnPageStarted(WebView view, string url, Bitmap favicon)
{
if (_htmlViewRenderer == null)
{
return;
}
var args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = url }, url);
_htmlViewRenderer.ElementController.SendNavigating(args);
if (args.Cancel)
{
_htmlViewRenderer.Control.StopLoading();
}
else
{
base.OnPageStarted(view, url, favicon);
}
}
public override void OnPageFinished(WebView view, string url)
{
if (_htmlViewRenderer == null)
{
return;
}
base.OnPageFinished(view, url);
var source = new UrlWebViewSource { Url = url };
var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
_htmlViewRenderer.ElementController.SendNavigated(args);
}
private WebResourceResponse AuthenticateApiGatewayRequest(string url, string httpMethod, IDictionary<string, string> requestHeaders, Func<WebResourceResponse> originalRequest)
{
try
{
// since this method is not called in UI thread, executing async methods in a sync way wouldn't block the UI
var cts = new CancellationTokenSource(_htmlViewRenderer.ClientSettings.ApiGatewayTimeout);
var httpClient = _htmlViewRenderer.ApiGatewayHttpClientProvider.GetApiGatewayClient(cancellationToken: cts.Token).Result.HttpClient;
var requestMessage = new HttpRequestMessage(new HttpMethod(httpMethod), url);
var result = httpClient.SendAsync(requestMessage, cts.Token).Result;
var resultHeader = result.Content.Headers;
var stream = result.Content.ReadAsStreamAsync().Result;
return new WebResourceResponse(resultHeader.ContentType?.MediaType, resultHeader.ContentType?.CharSet, (int)result.StatusCode, result.ReasonPhrase, requestHeaders, stream);
}
catch (Exception ex)
{
Log.Logger.Error($"Error requesting {url} from webview. Error message: {ex.Message}");
return originalRequest();
}
}
}
}
The original webhook which is not going to work due to reflection but said i would include it anyway
using Android.OS;
using Java.Lang;
using Java.Lang.Reflect;
using Serilog;
namespace APPNAME.Helpers
{
internal static class WebViewHookHelper
{
private const string WebViewFactory = "android.webkit.WebViewFactory";
private const string ProviderInstance = "sProviderInstance";
private const string WebViewDelegate = "android.webkit.WebViewDelegate";
private const string GetProviderClass = "getProviderClass";
private const string GetFactoryClass = "getFactoryClass";
public static void HookWebView(ILogger logger)
{
try
{
var factoryClass = Class.ForName(WebViewFactory);
var field = factoryClass?.GetDeclaredField(ProviderInstance);
if (field != null)
{
field.Accessible = true;
Object sProviderInstance = field.Get(null);
if (sProviderInstance == null)
{
var getProviderClassMethod = GetProviderClassMethod(factoryClass, logger);
if (getProviderClassMethod != null)
{
//TryToSetProviderInstance(getProviderClassMethod, factoryClass, field, logger);
logger.Information("HI here");
}
}
else
{
logger.Debug($"{ProviderInstance} isn't null");
}
}
logger.Debug("Hook done!");
}
catch (Throwable e)
{
logger.Error(e, "WebView hook failed.");
}
}
private static Method GetProviderClassMethod(Class factoryClass, ILogger logger)
{
Method getProviderClassMethod = null;
if (Build.VERSION.SdkInt > BuildVersionCodes.LollipopMr1)
{
getProviderClassMethod = factoryClass.GetDeclaredMethod(GetProviderClass);
}
else if (Build.VERSION.SdkInt == BuildVersionCodes.LollipopMr1)
{
getProviderClassMethod = factoryClass.GetDeclaredMethod(GetFactoryClass);
}
else
{
logger.Information("Don't need to Hook WebView");
}
return getProviderClassMethod;
}
private static void TryToSetProviderInstance(Method getProviderClassMethod, Class factoryClass, Field field, ILogger logger)
{
var delegateClass = Class.ForName(WebViewDelegate);
if (delegateClass != null)
{
getProviderClassMethod.Accessible = true;
var providerClass = getProviderClassMethod.Invoke(factoryClass) as Class;
var providerConstructor = providerClass?.GetDeclaredConstructor(delegateClass);
if (providerConstructor != null)
{
Object providerInstance = null;
providerConstructor.Accessible = true;
var declaredConstructor = delegateClass.GetDeclaredConstructor();
if (declaredConstructor != null)
{
declaredConstructor.Accessible = true;
var newInstance = declaredConstructor.NewInstance();
if (newInstance != null)
{
providerInstance = providerConstructor.NewInstance(newInstance);
}
}
logger.Debug("sProviderInstance:{}", providerInstance);
field.Set(ProviderInstance, providerInstance);
}
}
}
}
}