2017-10-25 09:29:36 +02:00
|
|
|
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
|
|
|
Gobbetti, Lem Dulfo, Uwe Hermann
|
|
|
|
|
|
|
|
This file is part of Gadgetbridge.
|
|
|
|
|
|
|
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Gadgetbridge is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
2016-12-28 20:53:17 +01:00
|
|
|
package nodomain.freeyourgadget.gadgetbridge.util;
|
|
|
|
|
2017-01-01 18:33:39 +01:00
|
|
|
import android.app.Activity;
|
2017-08-05 16:04:48 +02:00
|
|
|
import android.content.ComponentName;
|
2016-12-28 20:53:17 +01:00
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
2017-03-04 20:43:32 +01:00
|
|
|
import android.content.MutableContextWrapper;
|
2017-08-05 16:04:48 +02:00
|
|
|
import android.content.ServiceConnection;
|
2017-01-01 21:01:58 +01:00
|
|
|
import android.os.Build;
|
2017-08-05 16:04:48 +02:00
|
|
|
import android.os.Bundle;
|
2017-03-04 20:43:32 +01:00
|
|
|
import android.os.Handler;
|
2017-08-05 16:04:48 +02:00
|
|
|
import android.os.IBinder;
|
2017-03-04 20:43:32 +01:00
|
|
|
import android.os.Looper;
|
2017-08-05 16:04:48 +02:00
|
|
|
import android.os.Message;
|
|
|
|
import android.os.Messenger;
|
2017-02-25 18:01:08 +01:00
|
|
|
import android.support.annotation.NonNull;
|
2017-07-26 18:12:12 +02:00
|
|
|
import android.util.SparseArray;
|
2017-01-01 21:01:58 +01:00
|
|
|
import android.webkit.ValueCallback;
|
2017-01-02 12:07:57 +01:00
|
|
|
import android.webkit.WebResourceResponse;
|
2016-12-28 20:53:17 +01:00
|
|
|
import android.webkit.WebSettings;
|
|
|
|
import android.webkit.WebView;
|
2017-02-27 19:02:34 +01:00
|
|
|
|
2017-01-03 15:04:51 +01:00
|
|
|
import org.json.JSONArray;
|
2016-12-28 20:53:17 +01:00
|
|
|
import org.json.JSONException;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
2017-02-27 19:02:34 +01:00
|
|
|
import java.io.ByteArrayInputStream;
|
2016-12-28 20:53:17 +01:00
|
|
|
import java.io.File;
|
|
|
|
import java.io.IOException;
|
2017-10-13 21:57:22 +02:00
|
|
|
import java.nio.charset.Charset;
|
2017-01-03 15:04:51 +01:00
|
|
|
import java.util.HashMap;
|
2016-12-28 20:53:17 +01:00
|
|
|
import java.util.Iterator;
|
2017-07-26 18:12:12 +02:00
|
|
|
import java.util.Map;
|
2016-12-28 20:53:17 +01:00
|
|
|
import java.util.UUID;
|
2017-08-05 16:04:48 +02:00
|
|
|
import java.util.concurrent.CountDownLatch;
|
2016-12-28 20:53:17 +01:00
|
|
|
|
2017-02-28 21:11:26 +01:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage;
|
2016-12-28 20:53:17 +01:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
2017-09-25 17:12:35 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.JSInterface;
|
2016-12-28 20:53:17 +01:00
|
|
|
|
2017-02-25 21:50:05 +01:00
|
|
|
public class WebViewSingleton {
|
2016-12-28 20:53:17 +01:00
|
|
|
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(WebViewSingleton.class);
|
|
|
|
|
2017-07-26 18:12:12 +02:00
|
|
|
private WebView instance = null;
|
2017-03-04 20:43:32 +01:00
|
|
|
private MutableContextWrapper contextWrapper;
|
|
|
|
private Looper mainLooper;
|
2017-02-28 21:11:26 +01:00
|
|
|
private static WebViewSingleton webViewSingleton = new WebViewSingleton();
|
2017-03-04 19:46:18 +01:00
|
|
|
private static UUID currentRunningUUID;
|
2017-09-25 17:12:35 +02:00
|
|
|
public static Messenger internetHelper = null;
|
|
|
|
public static boolean internetHelperBound;
|
|
|
|
public static CountDownLatch latch;
|
|
|
|
public static WebResourceResponse internetResponse;
|
|
|
|
public final static Messenger internetHelperListener = new Messenger(new IncomingHandler());
|
2016-12-28 20:53:17 +01:00
|
|
|
|
|
|
|
private WebViewSingleton() {
|
|
|
|
}
|
|
|
|
|
2017-09-25 23:00:39 +02:00
|
|
|
public static synchronized void ensureCreated(Activity context) {
|
2017-07-26 18:12:12 +02:00
|
|
|
if (webViewSingleton.instance == null) {
|
2017-03-04 20:43:32 +01:00
|
|
|
webViewSingleton.contextWrapper = new MutableContextWrapper(context);
|
|
|
|
webViewSingleton.mainLooper = context.getMainLooper();
|
2017-07-26 18:12:12 +02:00
|
|
|
webViewSingleton.instance = new WebView(webViewSingleton.contextWrapper);
|
|
|
|
WebView.setWebContentsDebuggingEnabled(true);
|
|
|
|
webViewSingleton.instance.setWillNotDraw(true);
|
|
|
|
webViewSingleton.instance.clearCache(true);
|
|
|
|
webViewSingleton.instance.setWebViewClient(new GBWebClient());
|
|
|
|
webViewSingleton.instance.setWebChromeClient(new GBChromeClient());
|
|
|
|
WebSettings webSettings = webViewSingleton.instance.getSettings();
|
2017-02-25 18:01:08 +01:00
|
|
|
webSettings.setJavaScriptEnabled(true);
|
|
|
|
//needed to access the DOM
|
|
|
|
webSettings.setDomStorageEnabled(true);
|
|
|
|
//needed for localstorage
|
|
|
|
webSettings.setDatabaseEnabled(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-05 16:04:48 +02:00
|
|
|
//Internet helper outgoing connection
|
|
|
|
private static ServiceConnection internetHelperConnection = new ServiceConnection() {
|
|
|
|
public void onServiceConnected(ComponentName className, IBinder service) {
|
2017-08-10 00:33:54 +02:00
|
|
|
LOG.info("internet helper service bound");
|
2017-08-05 16:04:48 +02:00
|
|
|
internetHelperBound = true;
|
|
|
|
internetHelper = new Messenger(service);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onServiceDisconnected(ComponentName className) {
|
2017-08-10 00:33:54 +02:00
|
|
|
LOG.info("internet helper service unbound");
|
2017-08-05 16:04:48 +02:00
|
|
|
internetHelper = null;
|
|
|
|
internetHelperBound = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
//Internet helper inbound (responses) handler
|
2017-09-25 17:12:35 +02:00
|
|
|
private static class IncomingHandler extends Handler {
|
2017-10-13 21:57:22 +02:00
|
|
|
|
|
|
|
private String getCharsetFromHeaders(String contentType) {
|
|
|
|
if (contentType != null && contentType.toLowerCase().trim().contains("charset=")) {
|
|
|
|
String[] parts = contentType.toLowerCase().trim().split("=");
|
|
|
|
if (parts.length > 0)
|
|
|
|
return parts[1];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-08-05 16:04:48 +02:00
|
|
|
@Override
|
|
|
|
public void handleMessage(Message msg) {
|
|
|
|
Bundle data = msg.getData();
|
|
|
|
LOG.debug("WEBVIEW: internet helper returned: " + data.getString("response"));
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
|
|
headers.put("Access-Control-Allow-Origin", "*");
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
|
|
internetResponse = new WebResourceResponse(data.getString("content-type"), data.getString("content-encoding"), 200, "OK",
|
|
|
|
headers,
|
2017-10-13 21:57:22 +02:00
|
|
|
new ByteArrayInputStream(data.getString("response").getBytes(Charset.forName(getCharsetFromHeaders(data.getString("content-type")))))
|
2017-08-05 16:04:48 +02:00
|
|
|
);
|
|
|
|
} else {
|
2017-10-13 21:57:22 +02:00
|
|
|
internetResponse = new WebResourceResponse(data.getString("content-type"), data.getString("content-encoding"), new ByteArrayInputStream(data.getString("response").getBytes(Charset.forName(getCharsetFromHeaders(data.getString("content-type"))))));
|
2017-08-05 16:04:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
latch.countDown();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-25 18:01:08 +01:00
|
|
|
@NonNull
|
2017-09-25 17:12:35 +02:00
|
|
|
public static WebView getWebView(Context context) {
|
|
|
|
webViewSingleton.contextWrapper.setBaseContext(context);
|
2017-07-26 18:12:12 +02:00
|
|
|
return webViewSingleton.instance;
|
2017-02-25 18:01:08 +01:00
|
|
|
}
|
2017-02-25 15:44:37 +01:00
|
|
|
|
2018-01-13 21:04:42 +01:00
|
|
|
public static void runJavascriptInterface(@NonNull GBDevice device, @NonNull UUID uuid) {
|
2017-03-04 19:46:18 +01:00
|
|
|
if (uuid.equals(currentRunningUUID)) {
|
|
|
|
LOG.debug("WEBVIEW uuid not changed keeping the old context");
|
|
|
|
} else {
|
2017-07-24 23:57:07 +02:00
|
|
|
final JSInterface jsInterface = new JSInterface(device, uuid);
|
2017-03-04 19:46:18 +01:00
|
|
|
LOG.debug("WEBVIEW uuid changed, restarting");
|
|
|
|
currentRunningUUID = uuid;
|
2017-03-04 20:43:32 +01:00
|
|
|
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
2017-03-04 19:46:18 +01:00
|
|
|
@Override
|
|
|
|
public void run() {
|
2017-09-03 17:55:00 +02:00
|
|
|
webViewSingleton.instance.onResume();
|
2017-07-26 18:12:12 +02:00
|
|
|
webViewSingleton.instance.removeJavascriptInterface("GBjs");
|
|
|
|
webViewSingleton.instance.addJavascriptInterface(jsInterface, "GBjs");
|
|
|
|
webViewSingleton.instance.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500);
|
2017-03-04 19:46:18 +01:00
|
|
|
}
|
|
|
|
});
|
2017-09-03 17:55:00 +02:00
|
|
|
if (!internetHelperBound) {
|
|
|
|
Intent intent = new Intent();
|
|
|
|
intent.setComponent(new ComponentName("nodomain.freeyourgadget.internethelper", "nodomain.freeyourgadget.internethelper.MyService"));
|
|
|
|
webViewSingleton.contextWrapper.getApplicationContext().bindService(intent, internetHelperConnection, Context.BIND_AUTO_CREATE);
|
|
|
|
}
|
2017-03-04 19:46:18 +01:00
|
|
|
}
|
|
|
|
|
2016-12-28 20:53:17 +01:00
|
|
|
}
|
|
|
|
|
2017-02-28 21:11:26 +01:00
|
|
|
public static void appMessage(GBDeviceEventAppMessage message) {
|
2017-01-03 15:04:51 +01:00
|
|
|
|
2017-08-02 22:08:29 +02:00
|
|
|
final String jsEvent;
|
2017-07-26 18:12:12 +02:00
|
|
|
if (webViewSingleton.instance == null) {
|
2017-02-28 21:11:26 +01:00
|
|
|
LOG.warn("WEBVIEW is not initialized, cannot send appMessages to it");
|
2017-01-28 17:45:23 +01:00
|
|
|
return;
|
2017-02-28 21:11:26 +01:00
|
|
|
}
|
2017-02-26 17:57:26 +01:00
|
|
|
|
2017-08-02 22:08:29 +02:00
|
|
|
if (!message.appUUID.equals(currentRunningUUID)) {
|
|
|
|
LOG.info("WEBVIEW ignoring message for app that is not currently running: " + message.appUUID + " message: " + message.message + " type: " + message.type);
|
2017-08-02 23:09:43 +02:00
|
|
|
return;
|
2017-08-02 22:08:29 +02:00
|
|
|
}
|
|
|
|
|
2017-08-01 00:03:28 +02:00
|
|
|
// TODO: handle ACK and NACK types with ids
|
|
|
|
if (message.type != GBDeviceEventAppMessage.TYPE_APPMESSAGE) {
|
2017-08-02 22:08:29 +02:00
|
|
|
jsEvent = (GBDeviceEventAppMessage.TYPE_NACK == GBDeviceEventAppMessage.TYPE_APPMESSAGE) ? "NACK" + message.id : "ACK" + message.id;
|
|
|
|
LOG.debug("WEBVIEW received ACK/NACK:" + message.message + " for uuid: " + message.appUUID + " ID: " + message.id);
|
|
|
|
} else {
|
|
|
|
jsEvent = "appmessage";
|
2017-08-01 00:03:28 +02:00
|
|
|
}
|
|
|
|
|
2017-02-28 21:11:26 +01:00
|
|
|
final String appMessage = parseIncomingAppMessage(message.message, message.appUUID);
|
2017-08-02 22:08:29 +02:00
|
|
|
LOG.debug("to WEBVIEW: event: " + jsEvent + " message: " + appMessage);
|
2017-03-04 20:43:32 +01:00
|
|
|
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
2017-01-01 21:01:58 +01:00
|
|
|
@Override
|
|
|
|
public void run() {
|
2017-08-02 22:08:29 +02:00
|
|
|
webViewSingleton.instance.evaluateJavascript("Pebble.evaluate('" + jsEvent + "',[" + appMessage + "]);", new ValueCallback<String>() {
|
2017-02-28 21:11:26 +01:00
|
|
|
@Override
|
|
|
|
public void onReceiveValue(String s) {
|
|
|
|
//TODO: the message should be acked here instead of in PebbleIoThread
|
|
|
|
LOG.debug("Callback from appmessage: " + s);
|
|
|
|
}
|
|
|
|
});
|
2017-01-01 21:01:58 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2016-12-28 20:53:17 +01:00
|
|
|
|
2017-10-23 20:47:30 +02:00
|
|
|
public static void stopJavascriptInterface() {
|
|
|
|
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
if (webViewSingleton.instance != null) {
|
|
|
|
webViewSingleton.instance.removeJavascriptInterface("GBjs");
|
|
|
|
webViewSingleton.instance.loadUrl("about:blank");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-12-28 20:53:17 +01:00
|
|
|
public static void disposeWebView() {
|
2017-08-05 16:04:48 +02:00
|
|
|
if (internetHelperBound) {
|
|
|
|
LOG.debug("WEBVIEW: will unbind the internet helper");
|
|
|
|
webViewSingleton.contextWrapper.getApplicationContext().unbindService(internetHelperConnection);
|
2017-09-03 17:55:00 +02:00
|
|
|
internetHelperBound = false;
|
2017-08-05 16:04:48 +02:00
|
|
|
}
|
2017-09-03 17:55:00 +02:00
|
|
|
currentRunningUUID = null;
|
2017-03-04 20:43:32 +01:00
|
|
|
new Handler(webViewSingleton.mainLooper).post(new Runnable() {
|
2017-01-01 21:01:58 +01:00
|
|
|
@Override
|
|
|
|
public void run() {
|
2017-07-26 18:12:12 +02:00
|
|
|
if (webViewSingleton.instance != null) {
|
2017-09-03 17:55:00 +02:00
|
|
|
webViewSingleton.instance.removeJavascriptInterface("GBjs");
|
|
|
|
// webViewSingleton.instance.setWebChromeClient(null);
|
|
|
|
// webViewSingleton.instance.setWebViewClient(null);
|
2017-07-26 18:12:12 +02:00
|
|
|
webViewSingleton.instance.clearHistory();
|
|
|
|
webViewSingleton.instance.clearCache(true);
|
|
|
|
webViewSingleton.instance.loadUrl("about:blank");
|
|
|
|
// webViewSingleton.instance.freeMemory();
|
|
|
|
webViewSingleton.instance.pauseTimers();
|
2017-02-26 17:57:26 +01:00
|
|
|
// instance.destroy();
|
|
|
|
// instance = null;
|
|
|
|
// contextWrapper = null;
|
2017-02-28 21:11:26 +01:00
|
|
|
// jsInterface = null;
|
2017-01-01 21:01:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2016-12-28 20:53:17 +01:00
|
|
|
}
|
|
|
|
|
2017-09-25 17:12:35 +02:00
|
|
|
public static JSONObject getAppConfigurationKeys(UUID uuid) {
|
2016-12-28 20:53:17 +01:00
|
|
|
try {
|
2018-01-13 21:04:42 +01:00
|
|
|
File destDir = PebbleUtils.getPbwCacheDir();
|
2016-12-28 20:53:17 +01:00
|
|
|
File configurationFile = new File(destDir, uuid.toString() + ".json");
|
|
|
|
if (configurationFile.exists()) {
|
2017-07-26 18:12:12 +02:00
|
|
|
String jsonString = FileUtils.getStringFromFile(configurationFile);
|
|
|
|
JSONObject json = new JSONObject(jsonString);
|
2016-12-28 20:53:17 +01:00
|
|
|
return json.getJSONObject("appKeys");
|
|
|
|
}
|
|
|
|
} catch (IOException | JSONException e) {
|
2017-10-13 21:57:22 +02:00
|
|
|
LOG.warn("Unable to parse configuration JSON file", e);
|
2016-12-28 20:53:17 +01:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-07-26 18:12:12 +02:00
|
|
|
private static String parseIncomingAppMessage(String msg, UUID uuid) {
|
2017-02-28 21:11:26 +01:00
|
|
|
JSONObject jsAppMessage = new JSONObject();
|
|
|
|
|
|
|
|
JSONObject knownKeys = getAppConfigurationKeys(uuid);
|
2017-07-26 18:12:12 +02:00
|
|
|
SparseArray<String> appKeysMap = new SparseArray<>();
|
2017-02-28 21:11:26 +01:00
|
|
|
|
2017-10-13 21:57:22 +02:00
|
|
|
if (knownKeys == null || msg == null) {
|
2017-08-02 22:08:29 +02:00
|
|
|
return "{}";
|
|
|
|
}
|
|
|
|
|
2017-02-28 21:11:26 +01:00
|
|
|
String inKey, outKey;
|
|
|
|
//knownKeys contains "name"->"index", we need to reverse that
|
|
|
|
for (Iterator<String> key = knownKeys.keys(); key.hasNext(); ) {
|
|
|
|
inKey = key.next();
|
|
|
|
appKeysMap.put(knownKeys.optInt(inKey), inKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
JSONArray incoming = new JSONArray(msg);
|
|
|
|
JSONObject outgoing = new JSONObject();
|
|
|
|
for (int i = 0; i < incoming.length(); i++) {
|
|
|
|
JSONObject in = incoming.getJSONObject(i);
|
|
|
|
outKey = null;
|
|
|
|
Object outValue = null;
|
|
|
|
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
|
|
|
|
inKey = key.next();
|
|
|
|
switch (inKey) {
|
|
|
|
case "key":
|
|
|
|
outKey = appKeysMap.get(in.optInt(inKey));
|
|
|
|
break;
|
|
|
|
case "value":
|
|
|
|
outValue = in.get(inKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (outKey != null && outValue != null) {
|
|
|
|
outgoing.put(outKey, outValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
jsAppMessage.put("payload", outgoing);
|
|
|
|
|
2017-08-02 22:08:29 +02:00
|
|
|
} catch (Exception e) {
|
2017-10-13 21:57:22 +02:00
|
|
|
LOG.warn("Unable to parse incoming app message", e);
|
2017-02-28 21:11:26 +01:00
|
|
|
}
|
|
|
|
return jsAppMessage.toString();
|
|
|
|
}
|
|
|
|
|
2016-12-28 20:53:17 +01:00
|
|
|
}
|