📜 ⬆️ ⬇️

We are preparing a web application for the zoo versions of Android

More recently, and quite unexpectedly for myself, I was responsible for developing a program for Android. But I didn’t have to write to Java or Android at all before. It was necessary to make a web application, like phonegap and others, which almost completely works in the browser component. And all this under version 2.2 - 4.3 (SDK 8 - 18).

About some Android frills and crutches for them from the point of view of the person who saw all this for the first time, I would like to tell you. I hope it went off without HelloWorld, “OMG! Java ", etc.

Screen rotation / orientation change
Network unreachable
Ship local resources
Bridge between java and javascript
')

The entire program is essentially one WebView, in which local resources (assets) are loaded, and additional binding to it in Java. What JS cannot accomplish on its own is jerking over a Java <=> JS bridge synchronously / asynchronously. The latter can also “quietly” on the page.

Screen rotation


In Android, changing the orientation of the device leads to a re-creation of Activity (UI). For WebView, this causes the current page to be reloaded. Accordingly, the padding of input fields, the current scroll position, the state of JS, etc. are lost.
On the Internet, there are a lot of ways to deal with this situation, with different degrees of performance and taking into account different versions of the SDK. As a result, the following option came up working for all the necessary SDK versions:

In the manifest for the desired activity, we specify in android: configChanges "screenSize | orientation". It is both, otherwise there will be a version where nothing will work.
In layout, we do NOT describe WebView at all. In the place where our browser will be, we describe the stub:
<FrameLayout android:id="@+id/webViewPlaceholder" ... /> 

No browser - no problem with it :)

Now in the class of our Activity:
 protected FrameLayout mWebViewPlaceholder; //    protected WebView mWebView; //  -    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initUI(); } protected void initUI() { mWebViewPlaceholder = ((FrameLayout)findViewById(R.id.webViewPlaceholder)); if (mWebView == null) { mWebView = new WebView(this); mWebView.loadUrl("file:///android_asset/index.html"); } mWebViewPlaceholder.addView(mWebView); } @Override public void onConfigurationChanged(Configuration newConfig) { if (mWebView != null) { mWebViewPlaceholder.removeView(mWebView); //     WebView  UI } super.onConfigurationChanged(newConfig); setContentView(R.layout.activity_main); initUI(); //  WebView   UI } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mWebView.saveState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mWebView.restoreState(savedInstanceState); } 

What is the result?
When processing turn events and resolution changes, we temporarily pull out the WebView from the UI. At the same time, we independently save and restore the state of the WebView, since activity is destroyed when you go to the main screen of the system / another program, or open another activity in our program.
By the way, if you want to preserve the values ​​of the page elements (of the same input), then they should be assigned an id. Otherwise it will not be saved.

Network unreachable


If you open the page from your external servers in WebView, then the situation with the disconnected network on the device (airplane mode, wifi off ...) is quite funny. It seems to be catching WebViewClient.onReceivedError and swear. And if everything is calm and the WebViewClient.onPageFinished is called, then the page has loaded. This worked fine before, but on 4.3 (sdk 18) onReceivedError is called 2 minutes after the start of the download. I just didn’t try to find and change the settings for timeouts ... onPageStarted works, and then everything, silence for 2 minutes ...
Decision? Open a local resource, and from it already pump up the necessary from the server.
If anyone knows what's the matter - write in the comments, maybe even when it is useful.

Ship local resources


In WebView, without accessing the network, data can be loaded by passing either the loadData string ("...") or the address of the local resource loadUrl ("file: /// ..."). The first version with loadData unexpectedly showed itself with Cyrillic UTF-8. Despite the fact that loadData takes one of the parameters of the string encoding, in Android from 4.1+ (in 4.0 still, as far as I remember, it didn’t manifest), the page is clearly rendered in a single-byte encoding. And neither meta charset = utf-8, nor webView.getSettings (). SetDefaultTextEncodingName ("utf-8") did not correct the situation.
Decision? Load from file via loadUrl.

Bridge between java and javascript


To open a page in a standard browser component is, of course, an extremely heavy task, only preparation. Important is the interaction of JS code with Java, and at the initiative of any of the parties.
And there seems to be a handy thing - addJavascriptInterface (an example from the off documentation):
 class JsObject { @JavascriptInterface public String toString() { return "injectedObject"; } } webView.addJavascriptInterface(new JsObject(), "injectedObject"); webView.loadData("", "text/html", null); webView.loadUrl("javascript:alert(injectedObject.toString())"); 

We set "bridge" and its name for JS and everything, we use. Calling loadData without page content may seem strange, but the interaction interface is not initialized (injectedObject is not created in JS) until at least something is loaded (you can even have an empty string).
Passing parameters from JS to Java is also possible; the main match is the number and types of parameters.
We write, run in emulators and real devices: 4.3 - works, 4.2 - works, ... 2.3 - the application falls into the crust (crashes to the OS start screen) ... wait WHAT? ... 2.2 - works. Again 2.3 - in the crust.
A crash occurs when accessing injectedObject.toString (even without calling). The call to injectedObject is easy.
The appearance of “callJNIMethod <>” in the backtrace promises interesting leisure activities in the near future ...
Googling and SO scanning shows that the problem is massive and occurs with a probability close to 100% on the entire 2.3. * Line (sdk 9 - 10). It seems even sometimes on 2.2 (sdk 8), but I have met only a couple of such messages, but I don’t have a real device on this version, so I can’t confirm. And both on emulators and real devices. The main thing is to use WebView as a non-V8 JS engine.
The cries of the masses and the requests to Google to fix the bug began in 2010, but they did not release the official correction.

Over the years, several options have emerged for at least some workaround of the problem, workaround specifically under 2.3. *. Without considering the options, when the program cursed and refused to work under the affected versions, the solutions can be divided into the following approaches:

1. Instead of direct requests to Java, we perform, figuratively, the following:
  window.location = 'http://OurMegaPrefix:MethodName:Param0:Param1'; 

Then in Java such a request is processed through
 webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!url.startsWith("OurMegaPrefix")) { return false; } //  :  ,     ,   } //    : public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (javascriptInterfaceBroken) { String handleGingerbreadStupidity = "javascript:function openQuestion(id) { window.location='http://jshandler:openQuestion:'+id; }; " + "javascript: function handler() { this.openQuestion=openQuestion; }; " + "javascript: var jshandler = new handler();"; webView.loadUrl(handleGingerbreadStupidity); } } }); if (!javascriptInterfaceBroken) { webView.addJavascriptInterface(new JsObject(), "jshandler"); } 

After the page is loaded into onPageFinished, we inject the code into the page to implement the proxy object we need.
In addition to the frankly crutch decision, this method also does not allow making a synchronous call with obtaining a result. You can pass the name of the callback function ... But we will not do that, let's go further.

2. Another option is to use the standard javascript interface for receiving a synchronous (for js) response from the user: prompt (). The main thing to catch him :)
 var res = prompt('OurMegaPrefix:meth:param0:param1'); 

In res, somehow you need to save the result of the call from Java. And showing nothing to the user.
 webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { final String prefix = JSAPI.class.getSimpleName(); if (!message.startsWith(prefix)) { return super.onJsPrompt(view, url, message, defaultValue, result); } //  return true; //   prompt   } }); 

Here, JSAPI is the name of the proxy class (called “JsObject” above).
Having received one way or another the name of the method and the parameters (if any) it remains to call the corresponding method in JSAPI.
 var jsapi = new JSAPI(); 

The article is not about reflection in Java, so in short we do it this way:
 Method method = JSAPI.class.getMethod(methodName, new Class[] {String.class}); 

to call a method with one string parameter.
Or just search by name:
 Method method = null; for (Method meth : JSAPI.class.getMethods()) { if (meth.getName().equals(methodName)) { method = meth; break; } } 

Then we call the found method (again, an example for one parameter):
 method.invoke(jsapi, parameter); 


In general, this is enough for interfacing to bypass addJavascriptInterface. But I will offer my wrapper on all this business.
I myself am a backend (php, python, bash, C, etc.) and my JS is not so cool that it is not a shame for him. I will accept suggestions for improving the code :)

To transfer more complex objects than strings, use json:

And by DOMContentLoaded:
 if (typeof JSAPI == 'object') { return; } (function(){ var JSAPI = function() { function bridge(func) { var args = Array.prototype.slice.call(arguments.callee.caller.arguments, 0); var res = prompt('JSAPI.'+func, JSON.stringify(args)); try { res = JSON.parse(res); res = (res && res.result) ? res.result : null; } catch (e) { res = null; } return res; } this.getUserAccounts = function() { //    return bridge('getUserAccounts'); } }; window['JSAPI'] = new JSAPI(); })(window); 

If JSAPI is not declared (addJavascriptInterface was not called) - form a wrapper.
The essence of the wrapper - form the call prompt ('JSAPI.getUserAccounts', '[]'), where the second parameter is the json array of call parameters.
And the result of the prompt should return to us {result: ANSWER}.

Now, regardless of the Android SDK version, we call our method:
 var res = JSAPI.getUserAccounts(); //       json   : console.log(JSON.parse(res)[0]); 


On the Java side:
 webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { //     prompt(message='JSAPI.method', defaultValue='params') final String prefix = JSAPI.class.getSimpleName() + "."; if (!message.startsWith(prefix)) { return super.onJsPrompt(view, url, message, defaultValue, result); } final String meth_name = message.substring(prefix.length()); String json_result = null; try { //    json  final JSONArray params = new JSONArray(defaultValue); final int len = params.length(); final Object []paramValues = new Object[len]; for (int i = 0; i < len; ++i) { paramValues[i] = params.opt(i); } //  ,      Method method = null; for (Method meth : JSAPI.class.getMethods()) { if (meth.getName().equals(meth_name)) { method = meth; break; } } if (null == method) { throw new NoSuchMethodException(); } //      "{\"result\":}" final JSONObject res = new JSONObject(); res.put("result", method.invoke(new JSAPI(), paramValues)); json_result = res.toString(); } catch (JSONException e) { // ... } catch (NoSuchMethodException e) { // ... } catch (InvocationTargetException e) { // ... } catch (IllegalAccessException e) { // ... } result.confirm(json_result); return true; } }); 

And the method itself for the integrity of the example:
 class JSAPI { @JavascriptInterface public String getUserAccounts() { final JSONArray json = new JSONArray(); final String emailPattern = Patterns.EMAIL_ADDRESS.pattern(); final AccountManager am = AccountManager.get(getApplicationContext()); if (am != null) { for (Account ac : am.getAccounts()) { if (!ac.name.matches(emailPattern)) { continue; } final JSONArray item = new JSONArray(); item.put(ac.type); item.put(ac.name); json.put(item); } } return json.toString(); } } 

If the method accepts parameters, they must be specified. The result I always have for unification is set as a String, regardless of the returned result.
Annotation @JavascriptInterface needed for fresh versions of the SDK. In the old and without it worked.

As a conclusion, I also want to say that often the “broken android” condition included a code that checks Build.VERSION.RELEASE == "2.3". Bad check, better through Build.VERSION.RELEASE.startsWith ("2.3"). Better yet, IMHO, check the SDK version directly: (Build.VERSION.SDK_INT == 9) || (Build.VERSION.SDK_INT == 10).

Those who wish to check the JS assembly (on V8, I repeat, the bug is not reproduced) I suggest to google, everything is simple there.


And for a snack call initiated by Java: when needed, or as a callback.
 public void callJS(final String jsCode) { runOnUiThread(new Runnable() { @Override public void run() { webView.loadUrl("javascript:" + jsCode); } }); } jsapi.callJS("alert('Hello, Habr!');"); 

runOnUiThread is required if callJS is not called in the UI stream (almost as it will be).
But :) In 2.2, it worked without me. In older versions, it became more logical to fix.

This is how I write the first Java application :) I hope someone was interested or, suddenly, even useful.

PS Android Studio has recently grown to 0.2.11, and it is already quite possible to use it if you like JetBrains products. Not without unpleasant flaws, but quite functional.

PPS Some changes were made to the code samples based on the comments received.

Source: https://habr.com/ru/post/196546/


All Articles