I'm porting my jxbrowser integrations from 4.9x to 6.11.x, and i've got a problem with the JavaScript - JavaBridge with The IFrame. I register correctly with the follow code, and work as expect when I call the function from js in the main page. But doesn't work when the function is called from js inside the iframe.
browser.addScriptContextListener(new ScriptContextAdapter() {
#Override
public void onScriptContextCreated(ScriptContextEvent event) {
final Browser browser = event.getBrowser();
JSValue window = browser.executeJavaScriptAndReturnValue("window");
window.asObject().setProperty("resolveMsg", new JSFunctionCallback() {
#Override
public Object invoke(Object... params) {
String msgId = (String) params[0];
Builder builder = WrLocMsg.builder(msgId);
return builder.buildOriginalMessage();
}
});
}
});
Resolved by recovery window in this way:
JSValue window = browser.executeJavaScriptAndReturnValue(event.getJSContext().getFrameId(),"window");
Related
I want to implement a mechanism in a custom webview client (without JavaScript injection) that can block ads. Is a way I can catch ads and replace them with other ads from a trusted source?
Thanks
In your custom WebViewClient, you can override the function shouldInterceptRequest(WebView, WebResourceRequest).
From Android docs:
Notify the host application of a resource request and allow the application to return the data.
So the general idea is to check if the request is coming from an ad URL (plenty of black list filters out there), then return a "fake" resource that isn't the ad.
For a more in depth explanation plus an example, I recommend checking out this blog post.
To implement this, you have two options:
Use Javascript injected code to do this (which you explicitely said, don't want)
In WebView, instead of "http://example.com" load "http://myproxy.com?t=http://example.com" (properly escaped, of course) and setup "myproxy.com" to be a proxy which will fetch the upstream page (given in "t" query parameter, or in any other way) and replace ads with the trusted ones before sending response to the client. This will be pretty complex, though, because ads can be in many forms, they're usually Javascript injected themselves and you'd probably need to rewrite a lot of URL's in the fetched HTML, CSS and JS files etc.
I made a custom WebViewClient like:
public class MyWebViewClient extends WebViewClient {
#Override
public void onPageFinished(WebView view, String url) { }
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.endsWith(".mp4")) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), "video/*");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
view.getContext().startActivity(intent);
return true;
} else if (url.startsWith("tel:") || url.startsWith("sms:") || url.startsWith("smsto:")
|| url.startsWith("mms:") || url.startsWith("mmsto:")) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
view.getContext().startActivity(intent);
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
private Map<String, Boolean> loadedUrls = new HashMap<>();
#SuppressWarnings("deprecation")
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
boolean ad;
if (!loadedUrls.containsKey(url)) {
ad = AdBlocker.isAd(url);
loadedUrls.put(url, ad);
} else {
ad = loadedUrls.get(url);
}
return ad ? AdBlocker.createEmptyResource() :
super.shouldInterceptRequest(view, url);
}
}
And created an AdBlocker class like:
public class AdBlocker {
private static final Set<String> AD_HOSTS = new HashSet<>();
public static boolean isAd(String url) {
try {
return isAdHost(getHost(url));
} catch (MalformedURLException e) {
Log.e("Devangi..", e.toString());
return false;
}
}
private static boolean isAdHost(String host) {
if (TextUtils.isEmpty(host)) {
return false;
}
int index = host.indexOf(".");
return index >= 0 && (AD_HOSTS.contains(host) ||
index + 1 < host.length() && isAdHost(host.substring(index + 1)));
}
public static WebResourceResponse createEmptyResource() {
return new WebResourceResponse("text/plain", "utf-8", new ByteArrayInputStream("".getBytes()));
}
public static String getHost(String url) throws MalformedURLException {
return new URL(url).getHost();
}
}
And use this WebViewClient in your oncreate like:
webview.setWebViewClient(new MyWebViewClient());
I have been able to call JavaScript from C# inside the MainActivity but I'm trying to do so from an object. The majority of my app runs inside a WebView, my JavaScript calls to my C# Interface invoking an asynchronous function and when it's complete I would like to call back to my JavaScript but am unable to do so. Here is my current setup:
In my MainActivity I setup my WebView as such:
browser = FindViewById<WebView>(Resource.Id.mainView);
browser.SetInitialScale(1);
browser.SetWebChromeClient(new GeoWebChromeClient());
browser.Settings.UseWideViewPort = true;
browser.Settings.LoadWithOverviewMode = true;
if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
{
WebView.SetWebContentsDebuggingEnabled(true);
}
browser.Settings.SetGeolocationEnabled(true);
browser.Settings.JavaScriptEnabled = true;
browser.AddJavascriptInterface(new JSCSMedium(this, ref browser), "Android");
browser.LoadUrl("file:///android_asset/web/index.html");
Then inside the JSCSMedium object I have an asynch function:
[Export]
[JavascriptInterface]
public void SyncApps()
{
Task t = Task.Run(() => {
IList<ApplicationInfo> tempApps = Application.Context.PackageManager.GetInstalledApplications(PackageInfoFlags.MatchDirectBootAware);
string packageName = "";
string appName = "";
for (int i = 0; i < tempApps.Count(); i++)
{
packageName = tempApps[i].PackageName;
appName = tempApps[i].LoadLabel(manager);
var root = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
var filePath = System.IO.Path.Combine(root, "system");
filePath = System.IO.Path.Combine(filePath, packageName);
if (!System.IO.Directory.Exists(filePath))
{
System.IO.Directory.CreateDirectory(filePath);
}
filePath = System.IO.Path.Combine(filePath, "icon.png");
if (!System.IO.File.Exists(filePath))
{
Drawable icon = tempApps[i].LoadIcon(Application.Context.PackageManager);
BitmapDrawable bd = (BitmapDrawable)icon;
CreateAppIcon(bd.Bitmap, packageName);
}
Intent intent = Application.Context.PackageManager.GetLaunchIntentForPackage(packageName);
if (intent != null)
{
apps.Add(tempApps[i]);
}
}
});
}
If I don't do the C# as an async function it runs and returns data fine, but this process takes a bit of time and blocks the app temporarily. Inside my MainActivity I can call JavaScript just fine with:
browser.EvaluateJavascript("javascript: alert('fooBar');", null);
But browser is not accessible inside the JSCSMedium. I've tried passing the browser object as a reference and normally but it throws an exception stating that the EvaluateJavascript function must be called on the same thread as where it was instantiated. I've also tried sending a reference of my MainActivity to the JSCSMedium and call a function inside the MainActivity to run the EvaluateJavascript but it seems to do nothing. No error, not crash, just nothing.
The problem is Task.Run forces the code to run in the thread pool, and browser.EvaluateJavascript needs to run on the main thread.
You have at least two options here, depending on your needs:
1) Run the EvaluateJavascript call inside the Task.Run block with something like:
var h = new Handler(Looper.MainLooper);
h.Post(() => browser.EvaluateJavascript("javascript: alert('fooBar');", null));
2) Run the EvaluateJavascript call outside the Task.Run block:
[Export]
[JavascriptInterface]
public async void SyncApps()
{
await Task.Run(() => {
//...
});
browser.EvaluateJavascript("javascript: alert('fooBar');", null);
}
Not really sure if you can change the return type of SyncApps(). If JS doesn't complain, you better change that too.
After migrating JRE version 1.8.0_66 to 1.8.0_111 I've encountered an issue with making upcall from JavaScript to JavaFX.
Long story short: while there is a running background thread, WebView/WebEngine refuses to make JS-to-Java calls.
I use WebView to render HTML content which is generated from a domain Data Model (DM). Content contains elements with a handler assigned to it as follows:
<a href='#' onclick='explainHeadWord(this)'>some_word</a>
JS part looks like:
function explainHeadWord(hwElement) {
jsBridge.jsHandleQuery(hwElement.innerHTML);
}
function testBridge() {
jsBridge.jsTest();
}
where jsBridge is an inner Java class of the Controller
public class JSBridge {
public void jsHandleQuery(String headWord) {
log("jsBridge: jsHandleQuery: requested %s", headWord);
handleQuery(headWord);
}
public void jsTest() {
log("jsBridge: jsTest: test succeeded ");
}
}
which is injected as follows:
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
engine.setJavaScriptEnabled(true);
JSObject window = (JSObject) engine.executeScript("window");
window.setMember("jsBridge", new JSBridge());
//engine.executeScript("jsTest()");
//engine.executeScript("explainHeadWord(document.getElementsByTagName('a')[0])");
//engine.executeScript("jsBridge.jsHandleQuery(document.getElementsByTagName('a')[0])");
}
Besides the main DM I have an Index of cross-references which is Map<String, Collection<String>> built from DM, and a trigger method rebuilding that Index in the background each time DM changes. The first approach (which is worked fine on version 1.8.0_66) was based on ExecutorService:
private ExecutorService executor = Executors.newCachedThreadPool();
private Future<Boolean> indexer = executor.submit(() -> false);
...
private void rebuildIndex() {
executor.submit(() -> {
indexer.cancel(true);
indexer = executor.submit(() -> {
fullSearchIndex = getIndex();
if (isIndexingAborted()) return false;
return true;
});
try {
if (indexer.get()) {
log("resetIndex: Done");
updateTableView();
}
} catch (InterruptedException e) {
...
}
});
}
As was expected, clicking on an anchor in a WebView resulted to a JS-call jsBridge.jsHandleQuery(hwElement.innerHTML) and eventually to handleQuery(headWord) method call implemented in Controller. But after migrating JRE to version 1.8.0_111 WebView stopped to respond to an anchor clicking.
I've investigated logs and found that injecting jsBridge was successful as well as executing test scripts commented in the code below line window.setMember(). Clicking on an <a> element led to nothing. But without running test scripts (commented) there were records appeared in the log:
<record>
<date>2017-01-09T02:00:47</date>
<millis>1483920047169</millis>
<sequence>160</sequence>
<logger>com.sun.webkit.WebPage</logger>
<level>FINE</level>
<class>com.sun.webkit.WebPage</class>
<method>fwkAddMessageToConsole</method>
<thread>11</thread>
<message>fwkAddMessageToConsole(): message = TypeError: jsBridge.jsHandleQuery is not a function. (In 'jsBridge.jsHandleQuery(hwElement.innerHTML)', 'jsBridge.jsHandleQuery' is undefined), lineNumber = 26, sourceId = jar:file:/.../jar.jar!/view.js</message>
</record>
And after a moment the background (indexing) thread was complete and a content in the WebView was reloaded clicking on <a> elements starts to respond again - jsBridge.jsHandleQuery was executed.
The indexing thread executes getIndex() method that traverses DM and returns collected into Map data from DM. There's niether any interaction with FX application thread nor WebView depends on Index. Substituting fullSearchIndex = getMockIndex();
private Map<String, Collection<String>> getMockIndex() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) { }
return Collections.emptyMap();
}
in background thread doesn't change <a>'s behavior.
Next step was to refactor background thread into FX style by utilizing
javafx.concurrent.Service but the result is the same.
Will appreciate for pointing at what do I do wrong and how to tackle this issue.
Try to instantiate Bridge outside of the listener, i.e.,
final JSBridge bridge = new JSBridge();
engine.setJavaScriptEnabled(true);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
JSObject window = (JSObject) engine.executeScript("window");
window.setMember("jsBridge", bridge);
}
});
(Worked for me migrating from 1.8.0_91 to 1.8.0_121).
I have some ajax Behaviour that should pick some data using JS, and turn it back to Java. Sometimes it works but quite ofen it is just add url parameter and do page refresing/
public abstract class LoggedVKIdBehaviour extends AbstractDefaultAjaxBehavior {
private static final Logger logger = LoggerFactory.getLogger(LoggedVKIdBehaviour.class);
#Override
protected void respond(AjaxRequestTarget target) {
String loggedVkId = RequestCycle.get().getRequest().getRequestParameters().getParameterValue("logged_vkid").toString();
logger.info("ajax has comming with logged VK ID " + loggedVkId);
recived(target, loggedVkId);
}
protected abstract void recived(AjaxRequestTarget target, String loggedVkId);
#Override
public void renderHead(final Component component, IHeaderResponse response) {
super.renderHead(component, response);
Map<String, Object> map = new HashMap<>();
map.put("callbackFunction", getCallbackFunction(CallbackParameter.explicit("logged_vkid")));
//
PackageTextTemplate ptt = new PackageTextTemplate(LoggedVKIdBehaviour.class, "vkid_callback.js");
OnDomReadyHeaderItem onDomReadyHeaderItem = OnDomReadyHeaderItem.forScript(ptt.asString(map));
response.render(onDomReadyHeaderItem);
}
}
js template
var calback = ${callbackFunction};
var logged_vk_id = 11;
function authInfo(response) {
if (response.session) {
logged_vk_id = response.session.mid;
calback(response.session.mid);
console.log("recived callback from VK " + logged_vk_id);
}
}
$(document).ready(function () {
VK.Auth.getLoginStatus(authInfo);
});
it is do recursive redirection like http://localhost:8080/mytool/product/1?logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332773...
As i understand Ajaj technology - iti asynchronus requests, that shouldn't touch main url at all. So what is the reason for page refreshing?
this is generated Callback function
function (logged_vkid) {
var attrs = {"u":"../wicket/bookmarkable/com.tac.kulik.pages.product.ProductPage?12-1.IBehaviorListener.0-&productID=1"};
var params = [{"name":"logged_vkid","value":logged_vkid}];
attrs.ep = params.concat(attrs.ep || []);
Wicket.Ajax.ajax(attrs);
}
I use wicket 7.2
I did a lot investigations for few days. And found that when i remove
setPageManagerProvider(new NoSerializationPageManagerProvider(this));
Application throw me exepton in polite logs
org.apache.wicket.WicketRuntimeException: A problem occurred while
trying to collect debug information about not serializable object look
like it is could come from aused by: java.io.NotSerializableException:
com.tac.kulik.panel.smaccounts.SMAccountsPanel$1
which means that page tryed to be serialized for SOME REASON but $1 it is mean Anonimous class. I had few class created anonimously to ges some ajax links coming from ListView to be managed on parent panel. So After removing this Anonimous class logic, everything start and run well.
So i am happy, but still don't understand which reason page did serialization after ajax, and what the reason was to refresh whole page.
I work on an application that using PhoneGap. For the moment, I only test it on Android. I have several pages in my application that need the geolocation feature.
So I made a JS to handle it with this line of code (of course it's not my unique line of code) :
navigator.geolocation.watchPosition(successGeolocation, errorGeolocation, {maximumAge: 5000, enableHighAccuracy: true});
The geolocation need to be the most accurate possible so I use the GPS and it can take some time to have a GPS Fix.
The problem is when the user navigates from one page to another. The WatchPosition stop (it's normal because the user load an other page) and when I recall it the GPS need to Fix again.
It's very annoying and I search a solution to keep the GPS active. Someone has an idea for me ? Maybe with a plugin or a native Android LoC I can keep it active during all the application life ?
Thanks.
First step you should create an Android plugin that will give you API for receiving the location data.
Creating a plugin is quite easy and is explained here:
Plugin Development Guide and Developing a Plugin on Android.
You can also see an example of creating a Cordova plugin here.
Next, create a location monitor class. You can make it singleton and initialize it from your main activity.
Here is a simple, but working code I compiled from several sources and many tests to fit my needs.
The main code is taken from here, though I simplified it as much as was possible.
public class LocationMonitor
{
private LocationListener locationListener = null;
private LocationManager locationManager = null;
private Location location = null;
public LocationMonitor()
{
}
public void startGPSActivity(Context context)
{
LocationLooper looper = new LocationLooper();
looper.start();
while (!looper.isReady())
{
}
looper.handler.post(new LocationBootstrapper(context));
}
public void stopGPSActivity()
{
locationManager.removeUpdates(locationListener);
}
public Location getLocation()
{
return location;
}
private class LocationLooper extends Thread
{
private Handler handler;
private LocationLooper()
{
}
public void run()
{
Looper.prepare();
this.handler = new Handler();
Looper.loop();
}
public boolean isReady()
{
return this.handler != null;
}
}
private class LocationBootstrapper implements Runnable
{
private Context context;
private LocationBootstrapper(Context context)
{
this.context = context;
}
public void run()
{
locationListener = new LocationListenerImpl();
locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 1, locationListener);
}
}
private class LocationListenerImpl implements LocationListener
{
private LocationListenerImpl()
{
}
#Override
public void onLocationChanged(Location location)
{
LocationMonitor.this.location = location;
Log.i("LocationMonitor", "New location: lat= " + location.getLatitude() + " lng=" + location.getLongitude() + " acc=" + location.getAccuracy());
}
#Override
public void onProviderDisabled(String provider)
{
}
#Override
public void onProviderEnabled(String provider)
{
}
#Override
public void onStatusChanged(String provider, int status, Bundle extras)
{
}
}
}
Now access the LocationMonitor class from your plugin and you have your desired solution - page changes will not re-initialize your GPS and location data is available to your PhoneGap app.
Cheers