From 164c5e52a491160399c40a7708f40031248190e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Mon, 31 Oct 2022 12:09:36 +0000 Subject: [PATCH] Zepp OS: Add World Clocks --- .../gadgetbridge/daogen/GBDaoGenerator.java | 4 +- .../activities/ConfigureWorldClocks.java | 3 + .../activities/WorldClockDetails.java | 54 ++++++++++++-- .../schema/GadgetbridgeUpdate_45.java | 44 ++++++++++++ .../devices/AbstractDeviceCoordinator.java | 5 ++ .../devices/DeviceCoordinator.java | 6 ++ .../devices/huami/Huami2021Coordinator.java | 17 ++++- .../amazfitneo/AmazfitNeoCoordinator.java | 5 -- .../gadgetbridge/model/WorldClock.java | 2 + .../devices/huami/Huami2021Support.java | 5 +- .../service/devices/huami/HuamiSupport.java | 18 ++++- .../layout/activity_world_clock_details.xml | 70 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 + 13 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_45.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index bff6e5ae8..31fcbaba1 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -43,7 +43,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(44, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(45, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -591,6 +591,8 @@ public class GBDaoGenerator { indexUnique.makeUnique(); worldClock.addIndex(indexUnique); worldClock.addStringProperty("label").notNull(); + worldClock.addBooleanProperty("enabled"); + worldClock.addStringProperty("code"); worldClock.addStringProperty("timeZoneId").notNull(); worldClock.addToOne(user, userId); worldClock.addToOne(device, deviceId); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureWorldClocks.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureWorldClocks.java index 28c594873..5fd392edc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureWorldClocks.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureWorldClocks.java @@ -53,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.entities.WorldClock; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class ConfigureWorldClocks extends AbstractGBActivity { @@ -165,9 +166,11 @@ public class ConfigureWorldClocks extends AbstractGBActivity { private WorldClock createDefaultWorldClock(@NonNull Device device, @NonNull User user) { final WorldClock worldClock = new WorldClock(); final String timezone = TimeZone.getDefault().getID(); + worldClock.setEnabled(true); worldClock.setTimeZoneId(timezone); final String[] timezoneParts = timezone.split("/"); worldClock.setLabel(timezoneParts[timezoneParts.length - 1]); + worldClock.setCode(StringUtils.truncate(timezoneParts[timezoneParts.length - 1], 3).toUpperCase(Locale.getDefault())); worldClock.setDeviceId(device.getId()); worldClock.setUserId(user.getId()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java index a1f3c5646..f743935c0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java @@ -25,6 +25,7 @@ import android.text.TextWatcher; import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; +import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; @@ -43,6 +44,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.WorldClock; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class WorldClockDetails extends AbstractGBActivity { private static final Logger LOG = LoggerFactory.getLogger(WorldClockDetails.class); @@ -54,6 +56,9 @@ public class WorldClockDetails extends AbstractGBActivity { TextView worldClockTimezone; EditText worldClockLabel; + EditText worldClockCode; + View worldClockEnabledCard; + CheckBox worldClockEnabled; @Override protected void onCreate(Bundle savedInstanceState) { @@ -68,8 +73,11 @@ public class WorldClockDetails extends AbstractGBActivity { return; } + worldClockEnabledCard = findViewById(R.id.card_enabled); + worldClockEnabled = findViewById(R.id.world_clock_enabled); worldClockTimezone = findViewById(R.id.world_clock_timezone); worldClockLabel = findViewById(R.id.world_clock_label); + worldClockCode = findViewById(R.id.world_clock_code); device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE); final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); @@ -91,6 +99,14 @@ public class WorldClockDetails extends AbstractGBActivity { } }); + if (coordinator.supportsDisabledWorldClocks()) { + worldClockEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> { + worldClock.setEnabled(isChecked); + }); + } else { + worldClockEnabledCard.setVisibility(View.GONE); + } + worldClockLabel.setFilters(new InputFilter[]{new InputFilter.LengthFilter(coordinator.getWorldClocksLabelLength())}); worldClockLabel.addTextChangedListener(new TextWatcher() { @Override @@ -107,6 +123,22 @@ public class WorldClockDetails extends AbstractGBActivity { } }); + worldClockCode.setFilters(new InputFilter[]{new InputFilter.LengthFilter(3)}); + worldClockCode.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(final CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(final Editable s) { + worldClock.setCode(s.toString()); + } + }); + final FloatingActionButton fab = findViewById(R.id.fab_save); fab.setOnClickListener(new View.OnClickListener() { @Override @@ -150,25 +182,37 @@ public class WorldClockDetails extends AbstractGBActivity { } public void updateUiFromWorldClock() { + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); + final int maxLabelLength = coordinator.getWorldClocksLabelLength(); + + worldClockEnabled.setChecked(worldClock.getEnabled() == null || worldClock.getEnabled()); + final String oldTimezone = worldClockTimezone.getText().toString(); worldClockTimezone.setText(worldClock.getTimeZoneId()); // Check if the label was still the default (the timezone city name) // If so, and if the user changed the timezone, update the label to match the new city name - if (!oldTimezone.equals(worldClock.getTimeZoneId())) { + if (!StringUtils.isNullOrEmpty(oldTimezone) && !oldTimezone.equals(worldClock.getTimeZoneId())) { final String[] oldTimezoneParts = oldTimezone.split("/"); final String[] newTimezoneParts = worldClock.getTimeZoneId().split("/"); - final String newLabel = newTimezoneParts[newTimezoneParts.length - 1]; - final String oldLabel = oldTimezoneParts[oldTimezoneParts.length - 1]; + final String newLabel = StringUtils.truncate(newTimezoneParts[newTimezoneParts.length - 1], maxLabelLength); + final String oldLabel = StringUtils.truncate(oldTimezoneParts[oldTimezoneParts.length - 1], maxLabelLength); final String userLabel = worldClockLabel.getText().toString(); - - if (userLabel.equals(oldLabel)) { + if (StringUtils.isNullOrEmpty(userLabel) || userLabel.equals(oldLabel)) { // The label was still the original, so let's override it with the new city worldClock.setLabel(newLabel); } + final String newCode = StringUtils.truncate(newLabel, 3).toUpperCase(); + final String oldCode = StringUtils.truncate(oldLabel, 3).toUpperCase(); + final String userCode = worldClockCode.getText().toString(); + if (StringUtils.isNullOrEmpty(userCode) || userCode.equals(oldCode)) { + // The code was still the original, so let's override it with the new one + worldClock.setCode(newCode); + } } worldClockLabel.setText(worldClock.getLabel()); + worldClockCode.setText(worldClock.getCode()); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_45.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_45.java new file mode 100644 index 000000000..39b5ba85c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_45.java @@ -0,0 +1,44 @@ +/* Copyright (C) 2022 José Rebelo + + 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.database.schema; + +import android.database.sqlite.SQLiteDatabase; + +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript; +import nodomain.freeyourgadget.gadgetbridge.entities.WorldClockDao; + +public class GadgetbridgeUpdate_45 implements DBUpdateScript { + @Override + public void upgradeSchema(final SQLiteDatabase db) { + if (!DBHelper.existsColumn(WorldClockDao.TABLENAME, WorldClockDao.Properties.Code.columnName, db)) { + final String statement = "ALTER TABLE " + WorldClockDao.TABLENAME + " ADD COLUMN " + + WorldClockDao.Properties.Code.columnName + " TEXT"; + db.execSQL(statement); + } + + if (!DBHelper.existsColumn(WorldClockDao.TABLENAME, WorldClockDao.Properties.Enabled.columnName, db)) { + final String statement = "ALTER TABLE " + WorldClockDao.TABLENAME + " ADD COLUMN " + + WorldClockDao.Properties.Enabled.columnName + " BOOLEAN DEFAULT TRUE"; + db.execSQL(statement); + } + } + + @Override + public void downgradeSchema(final SQLiteDatabase db) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index ff2e85a35..8fd90bacc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -264,6 +264,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return 10; } + @Override + public boolean supportsDisabledWorldClocks() { + return false; + } + @Override public boolean supportsRgbLedColor() { return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 2efa012ee..8704eb224 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -397,6 +397,12 @@ public interface DeviceCoordinator { */ int getWorldClocksLabelLength(); + /** + * Indicates whether the device supports disabled world clocks that can be enabled through + * a menu on the device. + */ + boolean supportsDisabledWorldClocks(); + /** * Indicates whether the device has an led which supports custom colors */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java index c4b869d49..0abd61d80 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java @@ -85,8 +85,17 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { @Override public int getWorldClocksSlotCount() { - // TODO: It's supported, but not implemented - even in the official app - return 0; + return 20; // as enforced by Zepp + } + + @Override + public int getWorldClocksLabelLength() { + return 30; // at least + } + + @Override + public boolean supportsDisabledWorldClocks() { + return true; } @Override @@ -162,7 +171,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { settings.add(R.xml.devicesettings_header_time); //settings.add(R.xml.devicesettings_timeformat); settings.add(R.xml.devicesettings_dateformat_2); - // TODO settings.add(R.xml.devicesettings_world_clocks); + if (getWorldClocksSlotCount() > 0) { + settings.add(R.xml.devicesettings_world_clocks); + } // // Display diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitneo/AmazfitNeoCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitneo/AmazfitNeoCoordinator.java index 8d2c548c1..6d43ffa6d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitneo/AmazfitNeoCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitneo/AmazfitNeoCoordinator.java @@ -87,11 +87,6 @@ public class AmazfitNeoCoordinator extends HuamiCoordinator { return 20; // max in Zepp app } - @Override - public int getWorldClocksLabelLength() { - return 3; // neo has 3 letter city codes - } - @Override public int getReminderSlotCount(final GBDevice device) { return 0; // Neo does not support reminders diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java index 1c6b0bd1d..7b21d9c2c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java @@ -24,7 +24,9 @@ public interface WorldClock extends Serializable { */ String EXTRA_WORLD_CLOCK = "world_clock"; + Boolean getEnabled(); String getWorldClockId(); String getLabel(); + String getCode(); String getTimeZoneId(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index 4a8ced8b3..67678c1c3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -785,9 +785,8 @@ public abstract class Huami2021Support extends HuamiSupport { } @Override - protected void sendWorldClocks(final TransactionBuilder builder, - final List clocks) { - // TODO not yet implemented + protected boolean isWorldClocksEncrypted() { + return true; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 7d9e31492..628c6ca68 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -1101,7 +1101,11 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements return; } - writeToChunked2021(builder, (short) 0x0008, baos.toByteArray(), false); + writeToChunked2021(builder, (short) 0x0008, baos.toByteArray(), isWorldClocksEncrypted()); + } + + protected boolean isWorldClocksEncrypted() { + return false; } private byte[] encodeWorldClock(final WorldClock clock) { @@ -1113,8 +1117,12 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements final TimeZone timezone = TimeZone.getTimeZone(clock.getTimeZoneId()); final ZoneId zoneId = ZoneId.of(clock.getTimeZoneId()); - // Usually the 3-letter city code (eg. LIS for Lisbon), but doesn't seem to be used in the UI (used in Amazfit Neo) - baos.write(StringUtils.truncate(clock.getLabel(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8)); + // Usually the 3-letter city code (eg. LIS for Lisbon) + if (clock.getCode() != null) { + baos.write(StringUtils.truncate(clock.getCode(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8)); + } else { + baos.write(StringUtils.truncate(clock.getLabel(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8)); + } baos.write(0x00); // Some other string? Seems to be empty @@ -1164,6 +1172,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements baos.write((byte) ((nextTransitionTs >> (i * 8)) & 0xff)); } + if (coordinator.supportsDisabledWorldClocks()) { + baos.write((byte) (clock.getEnabled() ? 0x01 : 0x00)); + } + return baos.toByteArray(); } catch (final IOException e) { throw new RuntimeException("This should never happen", e); diff --git a/app/src/main/res/layout/activity_world_clock_details.xml b/app/src/main/res/layout/activity_world_clock_details.xml index 93bc99fd6..addb2a295 100644 --- a/app/src/main/res/layout/activity_world_clock_details.xml +++ b/app/src/main/res/layout/activity_world_clock_details.xml @@ -6,7 +6,33 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ReminderDetails"> + tools:context="nodomain.freeyourgadget.gadgetbridge.activities.WorldClockDetails"> + + + + + + + + @@ -40,7 +66,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" - android:text="?" + android:text="" android:textAppearance="?android:attr/textAppearanceLarge" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/label_timezone" /> @@ -85,6 +111,44 @@ + + + + + + + + + + Are you sure you want to delete the world clock? No free slots The device has no free slots for world clocks (total slots: %1$s) + Enabled Time Zone Label + Code Alarm details Reminder details World Clock details