From e40bd79fbf9a0ed7ac3391414d225173fcc5ea01 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 13 Jun 2022 08:35:32 +0100 Subject: [PATCH] Bangle.js: Adding built-in app-loader view (available via app management icon). Only available on internet-enabled builds (it's a webview) --- app/src/main/AndroidManifest.xml | 5 + .../DeviceSettingsPreferenceConst.java | 1 + .../banglejs/AppsManagementActivity.java | 188 ++++++++++++++++++ .../devices/banglejs/BangleJSCoordinator.java | 14 +- .../banglejs/BangleJSDeviceSupport.java | 91 +++++++-- .../activity_banglejs_apps_management.xml | 16 ++ app/src/main/res/values/strings.xml | 2 + .../main/res/xml/devicesettings_banglejs.xml | 8 +- 8 files changed, 294 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java create mode 100644 app/src/main/res/layout/activity_banglejs_apps_management.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 120e6c741..0e0ab834b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -468,6 +468,11 @@ + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 3dd558dc3..a07af38d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -45,6 +45,7 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_DEVICE_INTENTS = "device_intents"; public static final String PREF_BANGLEJS_TEXT_BITMAP = "banglejs_text_bitmap"; + public static final String PREF_BANGLEJS_WEBVIEW_URL = "banglejs_webview_url"; public static final String PREF_DISCONNECT_NOTIFICATION = "disconnect_notification"; public static final String PREF_DISCONNECT_NOTIFICATION_START = "disconnect_notification_start"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java new file mode 100644 index 000000000..7e9e8105c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/AppsManagementActivity.java @@ -0,0 +1,188 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.banglejs; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTERNET_ACCESS; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_WEBVIEW_URL; + +public class AppsManagementActivity extends AbstractGBActivity { + private static final Logger LOG = LoggerFactory.getLogger(AppsManagementActivity.class); + + private WebView webView; + private GBDevice mGBDevice; + private DeviceCoordinator mCoordinator; + /// It seems we can get duplicate broadcasts sometimes - so this helps to avoid that + private int deviceRxSeq = -1; + + public AppsManagementActivity() { + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_banglejs_apps_management); + + Intent intent = getIntent(); + Bundle bundle = intent.getExtras(); + if (bundle != null) { + mGBDevice = bundle.getParcelable(GBDevice.EXTRA_DEVICE); + } else { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + mCoordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice); + } + + private void toast(String data) { + GB.toast(data, Toast.LENGTH_LONG, GB.INFO); + } + + @Override + protected void onPause() { + super.onPause(); + webView.destroy(); + webView = null; + LocalBroadcastManager.getInstance(this).unregisterReceiver(deviceUpdateReceiver); + finish(); + } + + @Override + protected void onResume() { + super.onResume(); + IntentFilter commandFilter = new IntentFilter(); + commandFilter.addAction(GBDevice.ACTION_DEVICE_CHANGED); + commandFilter.addAction(BangleJSDeviceSupport.BANGLEJS_COMMAND_RX); + LocalBroadcastManager.getInstance(this).registerReceiver(deviceUpdateReceiver, commandFilter); + initViews(); + } + + BroadcastReceiver deviceUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BangleJSDeviceSupport.BANGLEJS_COMMAND_RX: { + String data = String.valueOf(intent.getExtras().get("DATA")); + int seq = intent.getIntExtra("SEQ",0); + LOG.info("WebView TX: " + data + "("+seq+")"); + if (seq==deviceRxSeq) { + LOG.info("WebView TX DUPLICATE AND IGNORED"); + } else { + deviceRxSeq = seq; + bangleRxData(data); + } + break; + } + } + } + }; + + public class WebViewInterface { + Context mContext; + + WebViewInterface(Context c) { + mContext = c; + } + + /// Called from the WebView when data needs to be sent to the Bangle + @JavascriptInterface + public void bangleTx(String data) { + LOG.info("WebView RX: " + data); + bangleTxData(data); + } + + } + + // Called when data received from Bangle.js - push data to the WebView + public void bangleRxData(String data) { + JSONArray s = new JSONArray(); + s.put(data); + String ss = s.toString(); + final String js = "bangleRx("+ss.substring(1, ss.length()-1)+");"; + LOG.info("WebView TX cmd: " + js); + if (webView!=null) webView.post(new Runnable() { + @Override + public void run() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, null); + } else { + webView.loadUrl("javascript: "+js); + } + } + }); + } + + // Called to send data to Bangle.js + public void bangleTxData(String data) { + Intent intent = new Intent(BangleJSDeviceSupport.BANGLEJS_COMMAND_TX); + intent.putExtra("DATA", data); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private void initViews() { + //https://stackoverflow.com/questions/4325639/android-calling-javascript-functions-in-webview + webView = findViewById(R.id.webview); + webView.setWebViewClient(new WebViewClient()); + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDatabaseEnabled(true); + settings.setDomStorageEnabled(true); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + String databasePath = this.getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); + settings.setDatabasePath(databasePath); + webView.addJavascriptInterface(new WebViewInterface(this), "Android"); + webView.setWebContentsDebuggingEnabled(true); // FIXME + + Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress())); + String url = devicePrefs.getString(PREF_BANGLEJS_WEBVIEW_URL, "").trim(); + if (url.isEmpty()) url = "https://banglejs.com/apps/android.html"; + webView.loadUrl(url); + + webView.setWebViewClient(new WebViewClient(){ + public void onPageFinished(WebView view, String weburl){ + //webView.loadUrl("javascript:showToast('WebView in Espruino')"); + } + }); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java index ea5a39764..6a9416a97 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/banglejs/BangleJSCoordinator.java @@ -34,6 +34,7 @@ import java.util.Vector; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; @@ -135,21 +136,18 @@ public class BangleJSCoordinator extends AbstractDeviceCoordinator { return true; } - @Override - public boolean supportsAppsManagement() { - return false; - } - @Override public int getAlarmSlotCount() { return 10; } @Override - public Class getAppsManagementActivity() { - return null; - } + public boolean supportsAppsManagement() { return BuildConfig.INTERNET_ACCESS; } + @Override + public Class getAppsManagementActivity() { + return BuildConfig.INTERNET_ACCESS ? AppsManagementActivity.class : null; + } @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java index c9cdd7b56..647f96487 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java @@ -258,9 +258,8 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { /// Write a string of data, and chunk it up private void uartTx(TransactionBuilder builder, String str) { + byte[] bytes = str.getBytes(StandardCharsets.ISO_8859_1); LOG.info("UART TX: " + str); - byte[] bytes; - bytes = str.getBytes(StandardCharsets.ISO_8859_1); // FIXME: somehow this is still giving us UTF8 data when we put images in strings. Maybe JSON.stringify is converting to UTF-8? for (int i=0;i0) json += ","; + Object o = null; + try { + o = a.get(i); + } catch (JSONException e) { + LOG.warn("jsonToString array error: " + e.getLocalizedMessage()); + } + json += jsonToStringInternal(o); + } + return json+"]"; + } else if (v instanceof JSONObject) { + JSONObject obj = (JSONObject)v; + String json = "{"; + Iterator iter = obj.keys(); + while (iter.hasNext()) { + String key = iter.next(); + Object o = null; + try { + o = obj.get(key); + } catch (JSONException e) { + LOG.warn("jsonToString object error: " + e.getLocalizedMessage()); + } + json += key+":"+jsonToStringInternal(o); + if (iter.hasNext()) json+=","; + } + return json+"}"; + } // else int/double/null + return v.toString(); + } - /// Write a string of data, and chunk it up + /// Convert a JSON object to a JSON String (NOT 100% JSON compliant) public String jsonToString(JSONObject jsonObj) { - String json = jsonObj.toString(); - // toString creates '\u0000' instead of '\0' - // FIXME: there have got to be nicer ways of handling this - maybe we just make our own JSON.toString (see below) - json = json.replaceAll("\\\\u000([01234567])", "\\\\$1"); - json = json.replaceAll("\\\\u00([0123456789abcdef][0123456789abcdef])", "\\\\x$1"); - return json; - /*String json = "{"; - Iterator iter = jsonObj.keys(); - while (iter.hasNext()) { - String key = iter.next(); - Object v = jsonObj.get(key); - if (v instanceof Integer) { - // ... - } else // .. - if (iter.hasNext()) json+=","; - } - return json+"}";*/ + /* jsonObj.toString() works but breaks char codes>128 (encodes as UTF8?) and also uses + \u0000 when just \0 would do (and so on). + + So we do it manually, which can be more compact anyway. + This is JSON-ish, so not exactly as per JSON1 spec but good enough for Espruino. + */ + return jsonToStringInternal(jsonObj); } /// Write a JSON object of data @@ -327,10 +369,10 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { private void handleUartRxLine(String line) { LOG.info("UART RX LINE: " + line); - + if (line.length()==0) return; if (">Uncaught ReferenceError: \"GB\" is not defined".equals(line)) GB.toast(getContext(), "Gadgetbridge plugin not installed on Bangle.js", Toast.LENGTH_LONG, GB.ERROR); - else if (line.length() > 0 && line.charAt(0)=='{') { + else if (line.charAt(0)=='{') { // JSON - we hope! try { JSONObject json = new JSONObject(line); @@ -567,6 +609,11 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport { receivedLine = receivedLine.substring(p+1); handleUartRxLine(line); } + // Send an intent with new data + Intent intent = new Intent(BangleJSDeviceSupport.BANGLEJS_COMMAND_RX); + intent.putExtra("DATA", packetStr); + intent.putExtra("SEQ", bangleCommandSeq++); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } return false; } diff --git a/app/src/main/res/layout/activity_banglejs_apps_management.xml b/app/src/main/res/layout/activity_banglejs_apps_management.xml new file mode 100644 index 000000000..08188a975 --- /dev/null +++ b/app/src/main/res/layout/activity_banglejs_apps_management.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0ca45df0..e7eb1ca37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,6 +182,8 @@ Enable this if your device has no support for your language\'s font Text as Bitmaps If a word cannot be rendered with the watch\'s font, render it to a bitmap in Gadgetbridge and display the bitmap on the watch + App loader URL + If you want a custom app loader put your https://…/android.html URL here. Otherwise leave blank for https://banglejs.com/apps Right-To-Left Enable this if your device can not show right-to-left languages Right-To-Left Max Line Length diff --git a/app/src/main/res/xml/devicesettings_banglejs.xml b/app/src/main/res/xml/devicesettings_banglejs.xml index 9270f8baf..60395238b 100644 --- a/app/src/main/res/xml/devicesettings_banglejs.xml +++ b/app/src/main/res/xml/devicesettings_banglejs.xml @@ -6,4 +6,10 @@ android:key="banglejs_text_bitmap" android:summary="@string/pref_summary_banglejs_text_bitmap" android:title="@string/pref_title_banglejs_text_bitmap" /> - \ No newline at end of file + +