Bangle.js: add support for rendering to a text that can't be displayed into a bitmap and sending the bitmap over

# Conflicts:
#	app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSDeviceSupport.java
This commit is contained in:
Gordon Williams 2022-05-18 16:35:07 +01:00
parent ec3b54ef47
commit b324f40a78
5 changed files with 103 additions and 11 deletions

View File

@ -44,6 +44,8 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_DEVICE_INTERNET_ACCESS = "device_internet_access";
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_DISCONNECT_NOTIFICATION = "disconnect_notification";
public static final String PREF_DISCONNECT_NOTIFICATION_START = "disconnect_notification_start";
public static final String PREF_DISCONNECT_NOTIFICATION_END = "disconnect_notification_end";

View File

@ -171,6 +171,7 @@ public class BangleJSCoordinator extends AbstractDeviceCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
Vector<Integer> settings = new Vector<Integer>();
settings.add(R.xml.devicesettings_banglejs);
settings.add(R.xml.devicesettings_transliteration);
settings.add(R.xml.devicesettings_high_mtu);
if (BuildConfig.INTERNET_ACCESS)

View File

@ -23,6 +23,7 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@ -92,6 +93,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTERNET_ACCESS;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTENTS;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBHelper.*;
import javax.xml.xpath.XPath;
@ -155,6 +157,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
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;i<bytes.length;i+=mtuSize) {
int l = bytes.length-i;
if (l>mtuSize) l=mtuSize;
@ -164,11 +167,33 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
/// Write a string of data, and chunk it up
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<String> 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+"}";*/
}
/// Write a JSON object of data
private void uartTxJSON(String taskName, JSONObject json) {
try {
TransactionBuilder builder = performInitialized(taskName);
uartTx(builder, "\u0010GB("+json.toString()+")\n");
uartTx(builder, "\u0010GB("+jsonToString(json)+")\n");
builder.queue(getQueue());
} catch (IOException e) {
GB.toast(getContext(), "Error in "+taskName+": " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
@ -196,10 +221,14 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
// JSON - we hope!
try {
JSONObject json = new JSONObject(line);
LOG.info("UART RX JSON parsed successfully");
handleUartRxJSON(json);
} catch (JSONException e) {
LOG.info("UART RX JSON parse failure: "+ e.getLocalizedMessage());
GB.toast(getContext(), "Malformed JSON from Bangle.js: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
} else {
LOG.info("UART RX line started with "+(int)line.charAt(0)+" - ignoring");
}
}
@ -424,6 +453,29 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
return true;
}
public String renderUnicodeAsImage(String txt) {
if (txt==null) return null;
// If we're not doing conversion, pass this right back
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
if (!devicePrefs.getBoolean(PREF_BANGLEJS_TEXT_BITMAP, false))
return txt;
// Otherwise split up and check each word
String words[] = txt.split(" ");
for (int i=0;i<words.length;i++) {
boolean isRenderable = true;
for (int j=0;j<words[i].length();j++) {
char c = words[i].charAt(j);
// TODO: better check?
if (c<0 || c>255) isRenderable = false;
}
if (!isRenderable)
words[i] = "\0"+bitmapToEspruinoString(textToBitmap(words[i]));
}
return String.join(" ", words);
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
try {
@ -431,10 +483,10 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
o.put("t", "notify");
o.put("id", notificationSpec.getId());
o.put("src", notificationSpec.sourceName);
o.put("title", notificationSpec.title);
o.put("subject", notificationSpec.subject);
o.put("body", notificationSpec.body);
o.put("sender", notificationSpec.sender);
o.put("title", renderUnicodeAsImage(notificationSpec.title));
o.put("subject", renderUnicodeAsImage(notificationSpec.subject));
o.put("body", renderUnicodeAsImage(notificationSpec.body));
o.put("sender", renderUnicodeAsImage(notificationSpec.sender));
o.put("tel", notificationSpec.phoneNumber);
uartTxJSON("onNotification", o);
} catch (JSONException e) {
@ -503,7 +555,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
cmdName = field.getName().substring(5).toLowerCase();
} catch (IllegalAccessException e) {}
o.put("cmd", cmdName);
o.put("name", callSpec.name);
o.put("name", renderUnicodeAsImage(callSpec.name));
o.put("number", callSpec.number);
uartTxJSON("onSetCallState", o);
} catch (JSONException e) {
@ -540,9 +592,9 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
try {
JSONObject o = new JSONObject();
o.put("t", "musicinfo");
o.put("artist", musicSpec.artist);
o.put("album", musicSpec.album);
o.put("track", musicSpec.track);
o.put("artist", renderUnicodeAsImage(musicSpec.artist));
o.put("album", renderUnicodeAsImage(musicSpec.album));
o.put("track", renderUnicodeAsImage(musicSpec.track));
o.put("dur", musicSpec.duration);
o.put("c", musicSpec.trackCount);
o.put("n", musicSpec.trackNr);
@ -707,9 +759,23 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
public Bitmap textToBitmap(String text) {
Paint paint = new Paint(0); // Paint.ANTI_ALIAS_FLAG not wanted as 1bpp
paint.setTextSize(18);
paint.setColor(0xFFFFFFFF);
paint.setTextAlign(Paint.Align.LEFT);
float baseline = -paint.ascent(); // ascent() is negative
int width = (int) (paint.measureText(text) + 0.5f); // round
int height = (int) (baseline + paint.descent() + 0.5f);
Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(image);
canvas.drawText(text, 0, baseline, paint);
return image;
}
/** Convert an Android bitmap to a base64 string for use in Espruino.
* Currently only 1bpp, no scaling */
public static String bitmapToEspruino(Bitmap bitmap) {
public static byte[] bitmapToEspruinoArray(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
byte bmp[] = new byte[((height * width + 7) >> 3) + 3];
@ -732,7 +798,19 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
if (cn > 0) bmp[n++] = (byte)c;
//LOG.info("BMP: " + width + "x"+height+" n "+n);
// Convert to base64
return Base64.encodeToString(bmp, Base64.DEFAULT).replaceAll("\n","");
return bmp;
}
/** Convert an Android bitmap to a base64 string for use in Espruino.
* Currently only 1bpp, no scaling */
public static String bitmapToEspruinoString(Bitmap bitmap) {
return new String(bitmapToEspruinoArray(bitmap), StandardCharsets.ISO_8859_1);
}
/** Convert an Android bitmap to a base64 string for use in Espruino.
* Currently only 1bpp, no scaling */
public static String bitmapToEspruinoBase64(Bitmap bitmap) {
return Base64.encodeToString(bitmapToEspruinoArray(bitmap), Base64.DEFAULT).replaceAll("\n","");
}
/** Convert a drawable to a bitmap, for use with bitmapToEspruino */

View File

@ -180,6 +180,8 @@
<string name="pref_title_notification_use_as_allow">Allow notifications from selected apps</string>
<string name="pref_title_transliteration">Transliteration</string>
<string name="pref_summary_transliteration">Enable this if your device has no support for your language\'s font</string>
<string name="pref_title_banglejs_text_bitmap">Text as Bitmaps</string>
<string name="pref_summary_banglejs_text_bitmap">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</string>
<string name="pref_title_rtl">Right-To-Left</string>
<string name="pref_summary_rtl">Enable this if your device can not show right-to-left languages</string>
<string name="pref_rtl_max_line_length">Right-To-Left Max Line Length</string>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_translate"
android:key="banglejs_text_bitmap"
android:summary="@string/pref_summary_banglejs_text_bitmap"
android:title="@string/pref_title_banglejs_text_bitmap" />
</androidx.preference.PreferenceScreen>