From 1313392a84d2e450891a3d95c9943ff2fc6b8df1 Mon Sep 17 00:00:00 2001 From: Thomas Kuehne Date: Sat, 13 Jun 2026 22:31:49 +0000 Subject: [PATCH] add BundleUtils.addToBundle - deduplicate code from BangleJSDeviceSupport and IntentApiReceiver - add support for more standard types - add BangleJS support for simple JSON arrays and JSON null values: - `"nullValue" : null` - `"booleanArray" : [true, false]` - `"doubleArray" : [1.2, -3.4]` - `"longArray" : [12, -34]` - `"StringArray" : ["12", "xyz"]` --- .../externalevents/IntentApiReceiver.java | 23 +- .../banglejs/BangleJSDeviceSupport.java | 13 +- .../gadgetbridge/util/BundleUtils.java | 275 ++++++++++++++++++ .../gadgetbridge/util/BundleUtilsTest.java | 162 +++++++++++ 4 files changed, 443 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtils.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtilsTest.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java index ccb2081a12..999f4cd681 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java @@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; +import nodomain.freeyourgadget.gadgetbridge.util.BundleUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.backup.PeriodicZipExporter; @@ -328,27 +329,7 @@ public class IntentApiReceiver extends BroadcastReceiver { } String option = key.substring(prefix.length()); Object extra = extras.get(key); - if (extra == null) { - options.putSerializable(option, null); - } else if (extra instanceof String value) { - options.putString(option, value); - } else if (extra instanceof Boolean value) { - options.putBoolean(option, value); - } else if (extra instanceof Integer value) { - options.putInt(option, value); - } else if (extra instanceof Long value) { - options.putLong(option, value); - } else if (extra instanceof Float value) { - options.putFloat(option, value); - } else if (extra instanceof float[] value) { - options.putFloatArray(option, value); - } else if (extra instanceof int[] value) { - options.putIntArray(option, value); - } else if (extra instanceof long[] value) { - options.putLongArray(option, value); - } else if (extra instanceof Parcelable value) { - options.putParcelable(option, value); - } else { + if (!BundleUtils.addToBundle(options, key, extra)) { LOG.warn("unhandled extra {} {} {}", option, extra, extra.getClass()); } } 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 c22acd53e1..ae6af82f8c 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 @@ -141,6 +141,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDevic import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.BundleUtils; import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -1053,22 +1054,16 @@ public class BangleJSDeviceSupport extends AbstractBTLESingleDeviceSupport { if (json.has("extra")) { JSONObject extra = json.getJSONObject("extra"); Iterator iter = extra.keys(); + Bundle extras = new Bundle(); while (iter.hasNext()) { String key = iter.next(); Object value = extra.get(key); - if (value instanceof Boolean boolExtra) { - in.putExtra(key, boolExtra); - } else if (value instanceof Integer integerExtra) { - in.putExtra(key, integerExtra); - } else if (value instanceof Long longExtra) { - in.putExtra(key, longExtra); - } else if (value instanceof Double doubleExtra) { - in.putExtra(key, doubleExtra); - } else { + if (!BundleUtils.addToBundle(extras, key, value)) { in.putExtra(key, value.toString()); } } + in.putExtras(extras); } LOG.info("Executing intent:\n\t" + String.valueOf(in) + "\n\tTargeting: " + target); //GB.toast(getContext(), String.valueOf(in), Toast.LENGTH_LONG, GB.INFO); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtils.java new file mode 100644 index 0000000000..462abeb75c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtils.java @@ -0,0 +1,275 @@ +/* Copyright (C) 2026 Thomas Kuehne + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.util; + +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.util.Size; +import android.util.SizeF; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +public final class BundleUtils { + private static final Logger LOG = LoggerFactory.getLogger(BundleUtils.class); + + private BundleUtils() { + throw new UnsupportedOperationException("static utility class"); + } + + /// Put the {@code value} into the {@code bundle}. Supports simple {@link JSONObject} as well as + /// non-empty {@link JSONArray} that contain exclusively one of {@link Boolean}, {@link Double}, + /// {@link Integer}, {@link Long}, or {@link String}. + /// + /// @return {@code true} if the {@code value} was actually put into the {@code bundle} + public static boolean addToBundle(@NonNull final Bundle bundle, + @NonNull final String key, + @Nullable final Object value) { + if (value == null || JSONObject.NULL.equals(value)) { + bundle.putString(key, null); + return true; + } + + if (value instanceof final Boolean extra) { + bundle.putBoolean(key, extra); + return true; + } + + if (value instanceof final boolean[] extra) { + bundle.putBooleanArray(key, extra); + return true; + } + + if (value instanceof final Byte extra) { + bundle.putByte(key, extra); + return true; + } + + if (value instanceof final byte[] extra) { + bundle.putByteArray(key, extra); + return true; + } + + if (value instanceof final Short extra) { + bundle.putShort(key, extra); + return true; + } + + if (value instanceof final short[] extra) { + bundle.putShortArray(key, extra); + return true; + } + + if (value instanceof final Character extra) { + bundle.putChar(key, extra); + return true; + } + + if (value instanceof final char[] extra) { + bundle.putCharArray(key, extra); + return true; + } + + if (value instanceof final Integer extra) { + bundle.putInt(key, extra); + return true; + } + + if (value instanceof final int[] extra) { + bundle.putIntArray(key, extra); + return true; + } + + if (value instanceof final Long extra) { + bundle.putLong(key, extra); + return true; + } + + if (value instanceof final long[] extra) { + bundle.putLongArray(key, extra); + return true; + } + + if (value instanceof final Float extra) { + bundle.putFloat(key, extra); + return true; + } + + if (value instanceof final float[] extra) { + bundle.putFloatArray(key, extra); + return true; + } + + if (value instanceof final Double extra) { + bundle.putDouble(key, extra); + return true; + } + + if (value instanceof final double[] extra) { + bundle.putDoubleArray(key, extra); + return true; + } + + if (value instanceof final String extra) { + bundle.putString(key, extra); + return true; + } + + if (value instanceof final String[] extra) { + bundle.putStringArray(key, extra); + return true; + } + + if (value instanceof final CharSequence extra) { + bundle.putCharSequence(key, extra); + return true; + } + + if (value instanceof final CharSequence[] extra) { + bundle.putCharSequenceArray(key, extra); + return true; + } + + if (value instanceof final Bundle extra) { + bundle.putBundle(key, extra); + return true; + } + + if (value instanceof final SizeF extra) { + bundle.putSizeF(key, extra); + return true; + } + + if (value instanceof final IBinder extra) { + bundle.putBinder(key, extra); + return true; + } + + if (value instanceof final Parcelable extra) { + bundle.putParcelable(key, extra); + return true; + } + + if (value instanceof final Parcelable[] extra) { + bundle.putParcelableArray(key, extra); + return true; + } + + if (value instanceof final Serializable extra) { + bundle.putSerializable(key, extra); + return true; + } + + if (value instanceof final Size extra) { + bundle.putSize(key, extra); + return true; + } + + if (value instanceof final JSONArray jsonArray) { + try { + if (jsonArray.length() < 1) { + LOG.warn("empty array '{}' is unsupported", key); + return false; + } + + Object first = jsonArray.get(0); + if (first instanceof Boolean) { + boolean[] array = new boolean[jsonArray.length()]; + for (int arrayIndex = 0; arrayIndex < jsonArray.length(); arrayIndex++) { + Object element = jsonArray.get(arrayIndex); + if (element instanceof final Boolean x) { + array[arrayIndex] = x; + } else { + LOG.warn("boolean array '{}' contains unsupported value '{}' at index {}", + key, element, arrayIndex); + return false; + } + } + bundle.putBooleanArray(key, array); + return true; + } + + if (first instanceof Double) { + double[] array = new double[jsonArray.length()]; + for (int arrayIndex = 0; arrayIndex < jsonArray.length(); arrayIndex++) { + Object element = jsonArray.get(arrayIndex); + if (element instanceof final Double x) { + array[arrayIndex] = x; + } else { + LOG.warn("double array '{}' contains unsupported value '{}' at index {}", + key, element, arrayIndex); + return false; + } + } + bundle.putDoubleArray(key, array); + return true; + } + + if (first instanceof Integer || first instanceof Long) { + long[] array = new long[jsonArray.length()]; + for (int arrayIndex = 0; arrayIndex < jsonArray.length(); arrayIndex++) { + Object element = jsonArray.get(arrayIndex); + if (element instanceof final Long x) { + array[arrayIndex] = x; + } else if (element instanceof final Integer x) { + array[arrayIndex] = x.longValue(); + } else { + LOG.warn("long array '{}' contains unsupported value '{}' at index {}", + key, element, arrayIndex); + return false; + } + } + bundle.putLongArray(key, array); + return true; + } + + if (first instanceof String) { + String[] array = new String[jsonArray.length()]; + for (int arrayIndex = 0; arrayIndex < jsonArray.length(); arrayIndex++) { + Object element = jsonArray.get(arrayIndex); + if (element instanceof final String x) { + array[arrayIndex] = x; + } else if (element == null) { + array[arrayIndex] = null; + } else { + LOG.warn("String array '{}' contains unsupported value '{}' at index {}", + key, element, arrayIndex); + return false; + } + } + bundle.putStringArray(key, array); + return true; + } + } catch (final JSONException exception) { + LOG.warn("failed to add JSONArray: {}", exception.getLocalizedMessage(), exception); + return false; + } + } + + LOG.warn("failed to add {}: {}", value.getClass(), value); + return false; + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtilsTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtilsTest.java new file mode 100644 index 0000000000..8621fe797b --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/BundleUtilsTest.java @@ -0,0 +1,162 @@ +/* Copyright (C) 2026 Thomas Kuehne + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.util; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static nodomain.freeyourgadget.gadgetbridge.util.BundleUtils.addToBundle; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Size; +import android.util.SizeF; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; + +import java.io.Serial; +import java.io.Serializable; +import java.nio.CharBuffer; + +import nodomain.freeyourgadget.gadgetbridge.test.TestBase; + +public class BundleUtilsTest extends TestBase { + @Test + public void testCommonTypes() { + Object[] values = { + null, + true, + new boolean[]{true, false}, + (byte) 12, + new byte[]{12, 23}, + (short) 1234, + new short[]{1234, 2345}, + (char) 1234, + new char[]{1234, 2345}, + 123456, + new int[]{123456}, + 123456789L, + new long[]{123456789L}, + 1.1f, + new float[]{1.1f, 1.2f}, + 2.1, + new double[]{2.1, 2.2}, + "abc", + new String[]{"abc"}, + Uri.parse("http://example.com"), + new Parcelable[]{Uri.parse("http://example.com")}, + CharBuffer.wrap("abc"), + new CharBuffer[]{CharBuffer.wrap("abc")}, + new Serializable() { + @Serial + private static final long serialVersionUID = 8578229259758102775L; + }, + new Bundle(), + new SizeF(1.2f, 3.4f), + new Size(1, 3), + }; + + + for (int i = 0; i < values.length; i++) { + Bundle bundle = new Bundle(); + Object value = values[i]; + assertTrue("add[" + i + "] " + value, addToBundle(bundle, "key", value)); + Object actual = bundle.get("key"); + assertEquals("check[" + i + "]", value, actual); + assertEquals(1, bundle.size()); + } + + { + Bundle bundle = new Bundle(); + assertFalse(addToBundle(bundle, "object", new Object())); + assertEquals(0, bundle.size()); + } + } + + @Test + public void testJson() throws JSONException { + JSONObject[] objects = { + new JSONObject().put("key", true), + new JSONObject().put("key", false), + new JSONObject().put("key", Integer.MAX_VALUE), + new JSONObject().put("key", Long.MAX_VALUE), + new JSONObject().put("key", 1.2), + new JSONObject().put("key", "xyz"), + new JSONObject("{\"key\": null }"), + }; + + for (int i = 0; i < objects.length; i++) { + Bundle bundle = new Bundle(); + JSONObject object = objects[i]; + Object value = object.get("key"); + assertTrue("add[" + i + "]: " + object, addToBundle(bundle, "key", value)); + Object actual = bundle.get("key"); + if(value == JSONObject.NULL){ + value = null; + } + assertEquals("check[" + i + "]: " + object, value, actual); + assertEquals(1, bundle.size()); + } + + { + Bundle bundle = new Bundle(); + Object value = new JSONObject("{ \"key\" : [false, true] }").get("key"); + assertTrue(addToBundle(bundle, "key", value)); + Object actual = bundle.get("key"); + assertArrayEquals(new boolean[]{false, true}, (boolean[]) actual); + assertEquals(1, bundle.size()); + } + + { + Bundle bundle = new Bundle(); + Object value = new JSONObject("{ \"key\" : [123, 9223372036854775807] }").get("key"); + assertTrue(addToBundle(bundle, "key", value)); + Object actual = bundle.get("key"); + assertArrayEquals(new long[]{123L, 9223372036854775807L}, (long[]) actual); + assertEquals(1, bundle.size()); + } + + { + Bundle bundle = new Bundle(); + Object value = new JSONObject("{ \"key\" : [1.2, 3.4] }").get("key"); + assertTrue(addToBundle(bundle, "key", value)); + Object actual = bundle.get("key"); + assertArrayEquals(new double[]{1.2, 3.4}, (double[]) actual, 0.001); + assertEquals(1, bundle.size()); + } + + { + Bundle bundle = new Bundle(); + Object value = new JSONObject("{ \"key\" : [\"123\", \"abc\"] }").get("key"); + assertTrue(addToBundle(bundle, "key", value)); + Object actual = bundle.get("key"); + assertArrayEquals(new String[]{"123", "abc"}, (String[]) actual); + assertEquals(1, bundle.size()); + } + + { + Bundle bundle = new Bundle(); + Object value = new JSONObject("{ \"key\" : [] }").get("key"); + assertFalse(addToBundle(bundle, "key", value)); + assertEquals(0, bundle.size()); + } + } +} \ No newline at end of file