From 9ee1aa87e8c5c87cc8c86e4d2519a8f255bb1969 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Sat, 13 Jan 2018 21:43:08 +0100 Subject: [PATCH] Move some methods to clarify responsibilities and simplify some things --- .../activities/ExternalPebbleJSActivity.java | 14 +- .../deviceevents/GBDeviceEventAppMessage.java | 10 + .../devices/pebble/PebbleIoThread.java | 38 +++- .../devices/pebble/webview/JSInterface.java | 3 +- .../gadgetbridge/util/PebbleUtils.java | 72 +++++++ .../gadgetbridge/util/WebViewSingleton.java | 181 ++++++------------ 6 files changed, 176 insertions(+), 142 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java index 9f5df5812..7c0dd0854 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java @@ -17,7 +17,6 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -90,7 +89,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity { } myWebView.setWillNotDraw(false); myWebView.removeJavascriptInterface("GBActivity"); - myWebView.addJavascriptInterface(new ActivityJSInterface(ExternalPebbleJSActivity.this), "GBActivity"); + myWebView.addJavascriptInterface(new ActivityJSInterface(), "GBActivity"); FrameLayout fl = (FrameLayout) findViewById(R.id.webview_placeholder); fl.addView(myWebView); @@ -122,7 +121,7 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity { JSInterface gbJSInterface = new JSInterface(currentDevice, currentUUID); myWebView.addJavascriptInterface(gbJSInterface, "GBjs"); - myWebView.addJavascriptInterface(new ActivityJSInterface(ExternalPebbleJSActivity.this), "GBActivity"); + myWebView.addJavascriptInterface(new ActivityJSInterface(), "GBActivity"); myWebView.loadUrl("file:///android_asset/app_config/configure.html"); @@ -163,16 +162,9 @@ public class ExternalPebbleJSActivity extends AbstractGBActivity { private class ActivityJSInterface { - Context mContext; - - ActivityJSInterface(Context c) { - mContext = c; - } - @JavascriptInterface public void closeActivity() { - NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext); + NavUtils.navigateUpFromSameTask(ExternalPebbleJSActivity.this); } } - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppMessage.java index 2dd3f3dd9..58028151c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppMessage.java @@ -27,4 +27,14 @@ public class GBDeviceEventAppMessage extends GBDeviceEvent { public UUID appUUID; public int id; public String message; + + @Override + public String toString() { + return "GBDeviceEventAppMessage{" + + "type=" + type + + ", appUUID=" + appUUID + + ", message='" + message + '\'' + + ", id=" + id + + '}'; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index 3bafa0312..c6f087320 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -25,6 +25,8 @@ import android.content.Intent; import android.net.Uri; import android.os.ParcelUuid; import android.support.v4.content.LocalBroadcastManager; +import android.webkit.ValueCallback; +import android.webkit.WebView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,7 +61,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble.PebbleLESupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; -import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -100,12 +101,45 @@ class PebbleIoThread extends GBDeviceIoThread { private int mBytesWritten = -1; private void sendAppMessageJS(GBDeviceEventAppMessage appMessage) { - WebViewSingleton.appMessage(appMessage); + sendAppMessage(appMessage); if (appMessage.type == GBDeviceEventAppMessage.TYPE_APPMESSAGE) { write(mPebbleProtocol.encodeApplicationMessageAck(appMessage.appUUID, (byte) appMessage.id)); } } + public static void sendAppMessage(GBDeviceEventAppMessage message) { + final String jsEvent; + try { + WebViewSingleton.checkAppRunning(message.appUUID); + } catch (IllegalStateException ex) { + LOG.warn("Unable to send app message: " + message, ex); + return; + } + + // TODO: handle ACK and NACK types with ids + if (message.type != GBDeviceEventAppMessage.TYPE_APPMESSAGE) { + 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"; + } + + final String appMessage = PebbleUtils.parseIncomingAppMessage(message.message, message.appUUID); + LOG.debug("to WEBVIEW: event: " + jsEvent + " message: " + appMessage); + WebViewSingleton.invokeWebview(new WebViewSingleton.WebViewRunnable() { + @Override + public void invoke(WebView webView) { + webView.evaluateJavascript("Pebble.evaluate('" + jsEvent + "',[" + appMessage + "]);", new ValueCallback() { + @Override + public void onReceiveValue(String s) { + //TODO: the message should be acked here instead of in PebbleIoThread + LOG.debug("Callback from appmessage: " + s); + } + }); + } + }); + } + PebbleIoThread(PebbleSupport pebbleSupport, GBDevice gbDevice, GBDeviceProtocol gbDeviceProtocol, BluetoothAdapter btAdapter, Context context) { super(gbDevice, context); mPebbleProtocol = (PebbleProtocol) gbDeviceProtocol; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/JSInterface.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/JSInterface.java index 5f4867260..e50606dbe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/JSInterface.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/JSInterface.java @@ -42,7 +42,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; -import nodomain.freeyourgadget.gadgetbridge.util.WebViewSingleton; public class JSInterface { @@ -73,7 +72,7 @@ public class JSInterface { public String sendAppMessage(String msg, String needsTransactionMsg) { boolean needsTransaction = "true".equals(needsTransactionMsg); LOG.debug("from WEBVIEW: " + msg + " needs a transaction: " + needsTransaction); - JSONObject knownKeys = WebViewSingleton.getAppConfigurationKeys(this.mUuid); + JSONObject knownKeys = PebbleUtils.getAppConfigurationKeys(this.mUuid); if (knownKeys == null) { LOG.warn("No app configuration keys for: " + mUuid); return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java index e5ec070c7..fcbee77c6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java @@ -17,11 +17,22 @@ package nodomain.freeyourgadget.gadgetbridge.util; import android.graphics.Color; +import android.util.SparseArray; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.util.Iterator; +import java.util.UUID; public class PebbleUtils { + private static final Logger LOG = LoggerFactory.getLogger(PebbleUtils.class); + public static String getPlatformName(String hwRev) { String platformName; if (hwRev.startsWith("snowy")) { @@ -102,4 +113,65 @@ public class PebbleUtils { public static File getPbwCacheDir() throws IOException { return new File(FileUtils.getExternalFilesDir(), "pbw-cache"); } + + public static JSONObject getAppConfigurationKeys(UUID uuid) { + try { + File destDir = getPbwCacheDir(); + File configurationFile = new File(destDir, uuid.toString() + ".json"); + if (configurationFile.exists()) { + String jsonString = FileUtils.getStringFromFile(configurationFile); + JSONObject json = new JSONObject(jsonString); + return json.getJSONObject("appKeys"); + } + } catch (IOException | JSONException e) { + LOG.warn("Unable to parse configuration JSON file", e); + } + return null; + } + + public static String parseIncomingAppMessage(String msg, UUID uuid) { + JSONObject jsAppMessage = new JSONObject(); + + JSONObject knownKeys = PebbleUtils.getAppConfigurationKeys(uuid); + SparseArray appKeysMap = new SparseArray<>(); + + if (knownKeys == null || msg == null) { + return "{}"; + } + + String inKey, outKey; + //knownKeys contains "name"->"index", we need to reverse that + for (Iterator 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 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); + + } catch (Exception e) { + LOG.warn("Unable to parse incoming app message", e); + } + return jsAppMessage.toString(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java index 46c654b93..c306e0367 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/WebViewSingleton.java @@ -31,29 +31,20 @@ import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.support.annotation.NonNull; -import android.util.SparseArray; -import android.webkit.ValueCallback; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; import java.nio.charset.Charset; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppMessage; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBChromeClient; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.GBWebClient; @@ -149,6 +140,20 @@ public class WebViewSingleton { return webViewSingleton.instance; } + /** + * Checks that the webview is up and the given app is running. + * @param uuid the uuid of the application expected to be running + * @throws IllegalStateException when the webview is not active or the app is not running + */ + public static void checkAppRunning(@NonNull UUID uuid) { + if (webViewSingleton.instance == null) { + throw new IllegalStateException("webViewSingleton.instance is null!"); + } + if (!uuid.equals(currentRunningUUID)) { + throw new IllegalStateException("Expected app " + uuid + " is not running, but " + currentRunningUUID + " is."); + } + } + public static void runJavascriptInterface(@NonNull GBDevice device, @NonNull UUID uuid) { if (uuid.equals(currentRunningUUID)) { LOG.debug("WEBVIEW uuid not changed keeping the old context"); @@ -156,13 +161,13 @@ public class WebViewSingleton { final JSInterface jsInterface = new JSInterface(device, uuid); LOG.debug("WEBVIEW uuid changed, restarting"); currentRunningUUID = uuid; - new Handler(webViewSingleton.mainLooper).post(new Runnable() { + invokeWebview(new WebViewRunnable() { @Override - public void run() { - webViewSingleton.instance.onResume(); - webViewSingleton.instance.removeJavascriptInterface("GBjs"); - webViewSingleton.instance.addJavascriptInterface(jsInterface, "GBjs"); - webViewSingleton.instance.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500); + public void invoke(WebView webView) { + webView.onResume(); + webView.removeJavascriptInterface("GBjs"); + webView.addJavascriptInterface(jsInterface, "GBjs"); + webView.loadUrl("file:///android_asset/app_config/configure.html?rand=" + Math.random() * 500); } }); if (!internetHelperBound) { @@ -174,51 +179,12 @@ public class WebViewSingleton { } - public static void appMessage(GBDeviceEventAppMessage message) { - - final String jsEvent; - if (webViewSingleton.instance == null) { - LOG.warn("WEBVIEW is not initialized, cannot send appMessages to it"); - return; - } - - 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); - return; - } - - // TODO: handle ACK and NACK types with ids - if (message.type != GBDeviceEventAppMessage.TYPE_APPMESSAGE) { - 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"; - } - - final String appMessage = parseIncomingAppMessage(message.message, message.appUUID); - LOG.debug("to WEBVIEW: event: " + jsEvent + " message: " + appMessage); - new Handler(webViewSingleton.mainLooper).post(new Runnable() { - @Override - public void run() { - webViewSingleton.instance.evaluateJavascript("Pebble.evaluate('" + jsEvent + "',[" + appMessage + "]);", new ValueCallback() { - @Override - public void onReceiveValue(String s) { - //TODO: the message should be acked here instead of in PebbleIoThread - LOG.debug("Callback from appmessage: " + s); - } - }); - } - }); - } - public static void stopJavascriptInterface() { - new Handler(webViewSingleton.mainLooper).post(new Runnable() { + invokeWebview(new WebViewRunnable() { @Override - public void run() { - if (webViewSingleton.instance != null) { - webViewSingleton.instance.removeJavascriptInterface("GBjs"); - webViewSingleton.instance.loadUrl("about:blank"); - } + public void invoke(WebView webView) { + webView.removeJavascriptInterface("GBjs"); + webView.loadUrl("about:blank"); } }); } @@ -230,86 +196,47 @@ public class WebViewSingleton { internetHelperBound = false; } currentRunningUUID = null; - new Handler(webViewSingleton.mainLooper).post(new Runnable() { + invokeWebview(new WebViewRunnable() { @Override - public void run() { - if (webViewSingleton.instance != null) { - webViewSingleton.instance.removeJavascriptInterface("GBjs"); -// webViewSingleton.instance.setWebChromeClient(null); -// webViewSingleton.instance.setWebViewClient(null); - webViewSingleton.instance.clearHistory(); - webViewSingleton.instance.clearCache(true); - webViewSingleton.instance.loadUrl("about:blank"); -// webViewSingleton.instance.freeMemory(); - webViewSingleton.instance.pauseTimers(); + public void invoke(WebView webView) { + webView.removeJavascriptInterface("GBjs"); +// webView.setWebChromeClient(null); +// webView.setWebViewClient(null); + webView.clearHistory(); + webView.clearCache(true); + webView.loadUrl("about:blank"); +// webView.freeMemory(); + webView.pauseTimers(); // instance.destroy(); // instance = null; // contextWrapper = null; // jsInterface = null; - } } }); } - public static JSONObject getAppConfigurationKeys(UUID uuid) { - try { - File destDir = PebbleUtils.getPbwCacheDir(); - File configurationFile = new File(destDir, uuid.toString() + ".json"); - if (configurationFile.exists()) { - String jsonString = FileUtils.getStringFromFile(configurationFile); - JSONObject json = new JSONObject(jsonString); - return json.getJSONObject("appKeys"); - } - } catch (IOException | JSONException e) { - LOG.warn("Unable to parse configuration JSON file", e); + public static void invokeWebview(final WebViewRunnable runnable) { + if (webViewSingleton.instance == null || webViewSingleton.mainLooper == null) { + LOG.warn("Webview already disposed, ignoring runnable"); + return; } - return null; + new Handler(webViewSingleton.mainLooper).post(new Runnable() { + @Override + public void run() { + if (webViewSingleton.instance == null) { + LOG.warn("Webview already disposed, cannot invoke runnable"); + return; + } + runnable.invoke(webViewSingleton.instance); + } + }); } - private static String parseIncomingAppMessage(String msg, UUID uuid) { - JSONObject jsAppMessage = new JSONObject(); - - JSONObject knownKeys = getAppConfigurationKeys(uuid); - SparseArray appKeysMap = new SparseArray<>(); - - if (knownKeys == null || msg == null) { - return "{}"; - } - - String inKey, outKey; - //knownKeys contains "name"->"index", we need to reverse that - for (Iterator 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 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); - - } catch (Exception e) { - LOG.warn("Unable to parse incoming app message", e); - } - return jsAppMessage.toString(); + public interface WebViewRunnable { + /** + * Called in the main thread with a non-null webView instance + * @param webView the webview, never null + */ + void invoke(WebView webView); } - }