diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
index b6f5fdef6..11cda37c9 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(37, MAIN_PACKAGE + ".entities");
+ final Schema schema = new Schema(38, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@@ -87,6 +87,7 @@ public class GBDaoGenerator {
addCalendarSyncState(schema, device);
addAlarms(schema, user, device);
addReminders(schema, user, device);
+ addWorldClocks(schema, user, device);
Entity notificationFilter = addNotificationFilters(schema);
@@ -561,6 +562,24 @@ public class GBDaoGenerator {
reminder.addToOne(device, deviceId);
}
+ private static void addWorldClocks(Schema schema, Entity user, Entity device) {
+ Entity worldClock = addEntity(schema, "WorldClock");
+ worldClock.implementsInterface("nodomain.freeyourgadget.gadgetbridge.model.WorldClock");
+ Property deviceId = worldClock.addLongProperty("deviceId").notNull().getProperty();
+ Property userId = worldClock.addLongProperty("userId").notNull().getProperty();
+ Property worldClockId = worldClock.addStringProperty("worldClockId").notNull().primaryKey().getProperty();
+ Index indexUnique = new Index();
+ indexUnique.addProperty(deviceId);
+ indexUnique.addProperty(userId);
+ indexUnique.addProperty(worldClockId);
+ indexUnique.makeUnique();
+ worldClock.addIndex(indexUnique);
+ worldClock.addStringProperty("label").notNull();
+ worldClock.addStringProperty("timeZoneId").notNull();
+ worldClock.addToOne(user, userId);
+ worldClock.addToOne(device, deviceId);
+ }
+
private static void addNotificationFilterEntry(Schema schema, Entity notificationFilterEntity) {
Entity notificatonFilterEntry = addEntity(schema, "NotificationFilterEntry");
notificatonFilterEntry.addIdProperty().autoincrement();
diff --git a/app/build.gradle b/app/build.gradle
index eb6da978e..aff0035ab 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -238,6 +238,10 @@ dependencies {
implementation 'com.google.protobuf:protobuf-lite:3.0.1'
implementation "androidx.multidex:multidex:2.0.1"
implementation 'com.android.volley:volley:1.2.1'
+
+ // JSR-310 timezones backport for Android, since we're still on java 7
+ implementation 'com.jakewharton.threetenabp:threetenabp:1.4.0'
+ testImplementation 'org.threeten:threetenbp:1.6.0'
}
preBuild.dependsOn(":GBDaoGenerator:genSources")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a2078b549..6a424dfb7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -498,6 +498,10 @@
android:name=".activities.ConfigureReminders"
android:label="@string/title_activity_set_reminders"
android:parentActivityName=".activities.ControlCenterv2" />
+
+
. */
+package nodomain.freeyourgadget.gadgetbridge.activities;
+
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.adapter.GBWorldClockListAdapter;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.entities.WorldClock;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+
+
+public class ConfigureWorldClocks extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(ConfigureWorldClocks.class);
+
+ private static final int REQ_CONFIGURE_WORLD_CLOCK = 1;
+
+ private GBWorldClockListAdapter mGBWorldClockListAdapter;
+ private GBDevice gbDevice;
+
+ private BroadcastReceiver timeTickBroadcastReceiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_configure_world_clocks);
+
+ gbDevice = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+
+ mGBWorldClockListAdapter = new GBWorldClockListAdapter(this);
+
+ final RecyclerView worldClocksRecyclerView = findViewById(R.id.world_clock_list);
+ worldClocksRecyclerView.setHasFixedSize(true);
+ worldClocksRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ worldClocksRecyclerView.setAdapter(mGBWorldClockListAdapter);
+ updateWorldClocksFromDB();
+
+ final FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+
+ int deviceSlots = coordinator.getWorldClocksSlotCount();
+
+ if (mGBWorldClockListAdapter.getItemCount() >= deviceSlots) {
+ // No more free slots
+ new AlertDialog.Builder(v.getContext())
+ .setTitle(R.string.world_clock_no_free_slots_title)
+ .setMessage(getBaseContext().getString(R.string.world_clock_no_free_slots_description, String.format(Locale.getDefault(), "%d", deviceSlots)))
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog, final int whichButton) {
+ }
+ })
+ .show();
+ return;
+ }
+
+ final WorldClock worldClock;
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ final Device device = DBHelper.getDevice(gbDevice, daoSession);
+ final User user = DBHelper.getUser(daoSession);
+ worldClock = createDefaultWorldClock(device, user);
+ } catch (final Exception e) {
+ LOG.error("Error accessing database", e);
+ return;
+ }
+
+ configureWorldClock(worldClock);
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ timeTickBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (Intent.ACTION_TIME_TICK.equals(intent.getAction())) {
+ // Refresh the UI, to update the current time in each timezone
+ mGBWorldClockListAdapter.notifyDataSetChanged();
+ }
+ }
+ };
+
+ registerReceiver(timeTickBroadcastReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ if (timeTickBroadcastReceiver != null) {
+ unregisterReceiver(timeTickBroadcastReceiver);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Refresh to update the current time on each clock
+ mGBWorldClockListAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQ_CONFIGURE_WORLD_CLOCK && resultCode == 1) {
+ updateWorldClocksFromDB();
+ sendWorldClocksToDevice();
+ }
+ }
+
+ private WorldClock createDefaultWorldClock(@NonNull Device device, @NonNull User user) {
+ final WorldClock worldClock = new WorldClock();
+ final String timezone = TimeZone.getDefault().getID();
+ worldClock.setTimeZoneId(timezone);
+ final String[] timezoneParts = timezone.split("/");
+ worldClock.setLabel(timezoneParts[timezoneParts.length - 1]);
+
+ worldClock.setDeviceId(device.getId());
+ worldClock.setUserId(user.getId());
+ worldClock.setWorldClockId(UUID.randomUUID().toString());
+
+ return worldClock;
+ }
+
+ /**
+ * Reads the available worldClocks from the database and updates the view afterwards.
+ */
+ private void updateWorldClocksFromDB() {
+ final List worldClocks = DBHelper.getWorldClocks(gbDevice);
+
+ mGBWorldClockListAdapter.setWorldClockList(worldClocks);
+ mGBWorldClockListAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // back button
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ public void configureWorldClock(final WorldClock worldClock) {
+ final Intent startIntent = new Intent(getApplicationContext(), WorldClockDetails.class);
+ startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice);
+ startIntent.putExtra(WorldClock.EXTRA_WORLD_CLOCK, worldClock);
+ startActivityForResult(startIntent, REQ_CONFIGURE_WORLD_CLOCK);
+ }
+
+ public void deleteWorldClock(final WorldClock worldClock) {
+ DBHelper.delete(worldClock);
+ updateWorldClocksFromDB();
+ sendWorldClocksToDevice();
+ }
+
+ private void sendWorldClocksToDevice() {
+ if (gbDevice.isInitialized()) {
+ GBApplication.deviceService().onSetWorldClocks(mGBWorldClockListAdapter.getWorldClockList());
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java
new file mode 100644
index 000000000..a1f3c5646
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WorldClockDetails.java
@@ -0,0 +1,174 @@
+/* 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.activities;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.TimeZone;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.WorldClock;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class WorldClockDetails extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(WorldClockDetails.class);
+
+ private WorldClock worldClock;
+ private GBDevice device;
+
+ ArrayAdapter timezoneAdapter;
+
+ TextView worldClockTimezone;
+ EditText worldClockLabel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_world_clock_details);
+
+ worldClock = (WorldClock) getIntent().getSerializableExtra(WorldClock.EXTRA_WORLD_CLOCK);
+
+ if (worldClock == null) {
+ GB.toast("No worldClock provided to WorldClockDetails Activity", Toast.LENGTH_LONG, GB.ERROR);
+ finish();
+ return;
+ }
+
+ worldClockTimezone = findViewById(R.id.world_clock_timezone);
+ worldClockLabel = findViewById(R.id.world_clock_label);
+
+ device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
+
+ final String[] timezoneIDs = TimeZone.getAvailableIDs();
+ timezoneAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, timezoneIDs);
+
+ final View cardTimezone = findViewById(R.id.card_timezone);
+ cardTimezone.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new AlertDialog.Builder(WorldClockDetails.this).setAdapter(timezoneAdapter, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ worldClock.setTimeZoneId(timezoneIDs[i]);
+ updateUiFromWorldClock();
+ }
+ }).create().show();
+ }
+ });
+
+ worldClockLabel.setFilters(new InputFilter[]{new InputFilter.LengthFilter(coordinator.getWorldClocksLabelLength())});
+ worldClockLabel.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.setLabel(s.toString());
+ }
+ });
+
+ final FloatingActionButton fab = findViewById(R.id.fab_save);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ updateWorldClock();
+ WorldClockDetails.this.setResult(1);
+ finish();
+ }
+ });
+
+ updateUiFromWorldClock();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // back button
+ // TODO confirm when exiting without saving
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void updateWorldClock() {
+ DBHelper.store(worldClock);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ state.putSerializable("worldClock", worldClock);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ worldClock = (WorldClock) savedInstanceState.getSerializable("worldClock");
+ updateUiFromWorldClock();
+ }
+
+ public void updateUiFromWorldClock() {
+ 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())) {
+ 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 userLabel = worldClockLabel.getText().toString();
+
+ if (userLabel.equals(oldLabel)) {
+ // The label was still the original, so let's override it with the new city
+ worldClock.setLabel(newLabel);
+ }
+ }
+
+ worldClockLabel.setText(worldClock.getLabel());
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java
index a34fde8bb..9ed08de5d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsActivity.java
@@ -48,20 +48,7 @@ public class DeviceSettingsActivity extends AbstractGBActivity implements
if (savedInstanceState == null) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(DeviceSpecificSettingsFragment.FRAGMENT_TAG);
if (fragment == null) {
- DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
- int[] supportedSettings = coordinator.getSupportedDeviceSpecificSettings(device);
- String[] supportedLanguages = coordinator.getSupportedLanguageSettings(device);
-
- if (supportedLanguages != null) {
- supportedSettings = ArrayUtils.insert(0, supportedSettings, R.xml.devicesettings_language_generic);
- }
- if (coordinator.supportsActivityTracking()) {
- supportedSettings = ArrayUtils.addAll(supportedSettings, R.xml.devicesettings_chartstabs);
- supportedSettings = ArrayUtils.addAll(supportedSettings, R.xml.devicesettings_device_card_activity_card_preferences);
- }
-
- final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);
- fragment = DeviceSpecificSettingsFragment.newInstance(device.getAddress(), supportedSettings, supportedLanguages, deviceSpecificSettingsCustomizer);
+ fragment = DeviceSpecificSettingsFragment.newInstance(device);
}
getSupportFragmentManager()
.beginTransaction()
@@ -73,21 +60,7 @@ public class DeviceSettingsActivity extends AbstractGBActivity implements
@Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen preferenceScreen) {
- DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
- int[] supportedSettings = coordinator.getSupportedDeviceSpecificSettings(device);
- String[] supportedLanguages = coordinator.getSupportedLanguageSettings(device);
-
- if (supportedLanguages != null) {
- supportedSettings = ArrayUtils.insert(0, supportedSettings, R.xml.devicesettings_language_generic);
- }
-
- if (coordinator.supportsActivityTracking()) {
- supportedSettings = ArrayUtils.addAll(supportedSettings, R.xml.devicesettings_chartstabs);
- supportedSettings = ArrayUtils.addAll(supportedSettings, R.xml.devicesettings_device_card_activity_card_preferences);
- }
-
- final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);
- PreferenceFragmentCompat fragment = DeviceSpecificSettingsFragment.newInstance(device.getAddress(), supportedSettings, supportedLanguages, deviceSpecificSettingsCustomizer);
+ final PreferenceFragmentCompat fragment = DeviceSpecificSettingsFragment.newInstance(device);
Bundle args = fragment.getArguments();
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.getKey());
fragment.setArguments(args);
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 1f82f62e6..39964bdcd 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
@@ -112,6 +112,8 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_KEY_VIBRATION = "key_vibration";
public static final String PREF_FAKE_RING_DURATION = "fake_ring_duration";
+ public static final String PREF_WORLD_CLOCKS = "pref_world_clocks";
+
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
public static final String PREF_HYDRATION_PERIOD = "pref_hydration_period";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
index 5f817cc9d..4bbd36b6a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
@@ -20,6 +20,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.InputType;
+import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
@@ -47,11 +48,17 @@ import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
+import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
+import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment;
@@ -89,6 +96,8 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
private DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer;
+ private GBDevice device;
+
private void setSettingsFileSuffix(String settingsFileSuffix, @NonNull int[] supportedSettings, String[] supportedLanguages) {
Bundle args = new Bundle();
args.putString("settingsFileSuffix", settingsFileSuffix);
@@ -103,6 +112,12 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
setArguments(args);
}
+ private void setDevice(final GBDevice device) {
+ final Bundle args = getArguments() != null ? getArguments() : new Bundle();
+ args.putParcelable("device", device);
+ setArguments(args);
+ }
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Bundle arguments = getArguments();
@@ -113,6 +128,7 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
int[] supportedSettings = arguments.getIntArray("supportedSettings");
String[] supportedLanguages = arguments.getStringArray("supportedLanguages");
this.deviceSpecificSettingsCustomizer = arguments.getParcelable("deviceSpecificSettingsCustomizer");
+ this.device = arguments.getParcelable("device");
if (settingsFileSuffix == null || supportedSettings == null) {
return;
@@ -587,6 +603,19 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
});
}
+ final Preference worldClocks = findPreference(PREF_WORLD_CLOCKS);
+ if (worldClocks != null) {
+ worldClocks.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final Intent intent = new Intent(getContext(), ConfigureWorldClocks.class);
+ intent.putExtra(GBDevice.EXTRA_DEVICE, device);
+ startActivity(intent);
+ return true;
+ }
+ });
+ }
+
final Preference cannedMessagesDismissCall = findPreference("canned_messages_dismisscall_send");
if (cannedMessagesDismissCall != null) {
cannedMessagesDismissCall.setOnPreferenceClickListener(new androidx.preference.Preference.OnPreferenceClickListener() {
@@ -704,13 +733,28 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
}
}
- static DeviceSpecificSettingsFragment newInstance(String settingsFileSuffix,
- @NonNull int[] supportedSettings,
- String[] supportedLanguages,
- DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer) {
+ static DeviceSpecificSettingsFragment newInstance(GBDevice device) {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
+ int[] supportedSettings = coordinator.getSupportedDeviceSpecificSettings(device);
+ String[] supportedLanguages = coordinator.getSupportedLanguageSettings(device);
+
+ if (supportedLanguages != null) {
+ supportedSettings = ArrayUtils.insert(0, supportedSettings, R.xml.devicesettings_language_generic);
+ }
+
+ if (coordinator.supportsActivityTracking()) {
+ supportedSettings = ArrayUtils.addAll(supportedSettings, R.xml.devicesettings_chartstabs);
+ supportedSettings = ArrayUtils.addAll(supportedSettings, R.xml.devicesettings_device_card_activity_card_preferences);
+ }
+
+ final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);
+
+ final String settingsFileSuffix = device.getAddress();
+
final DeviceSpecificSettingsFragment fragment = new DeviceSpecificSettingsFragment();
fragment.setSettingsFileSuffix(settingsFileSuffix, supportedSettings, supportedLanguages);
fragment.setDeviceSpecificSettingsCustomizer(deviceSpecificSettingsCustomizer);
+ fragment.setDevice(device);
return fragment;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBWorldClockListAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBWorldClockListAdapter.java
new file mode 100644
index 000000000..82f07cde9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBWorldClockListAdapter.java
@@ -0,0 +1,132 @@
+/* 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.adapter;
+
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.cardview.widget.CardView;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
+import nodomain.freeyourgadget.gadgetbridge.entities.WorldClock;
+
+/**
+ * Adapter for displaying WorldClock instances.
+ */
+public class GBWorldClockListAdapter extends RecyclerView.Adapter {
+
+ private final Context mContext;
+ private ArrayList worldClockList;
+
+ public GBWorldClockListAdapter(final Context context) {
+ this.mContext = context;
+ }
+
+ public void setWorldClockList(final List worldClocks) {
+ this.worldClockList = new ArrayList<>(worldClocks);
+ }
+
+ public ArrayList getWorldClockList() {
+ return worldClockList;
+ }
+
+ @NonNull
+ @Override
+ public GBWorldClockListAdapter.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
+ final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_world_clock, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
+ final WorldClock worldClock = worldClockList.get(position);
+
+ holder.container.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ((ConfigureWorldClocks) mContext).configureWorldClock(worldClock);
+ }
+ });
+
+ holder.container.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ new AlertDialog.Builder(v.getContext())
+ .setTitle(v.getContext().getString(R.string.world_clock_delete_confirm_title, worldClock.getLabel()))
+ .setMessage(R.string.world_clock_delete_confirm_description)
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog, final int whichButton) {
+ ((ConfigureWorldClocks) mContext).deleteWorldClock(worldClock);
+ }
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+
+ return true;
+ }
+ });
+
+ holder.worldClockLabel.setText(worldClock.getLabel());
+ holder.worldClockTimezone.setText(worldClock.getTimeZoneId());
+
+ final DateFormat df = new SimpleDateFormat("HH:mm", GBApplication.getLanguage());
+ df.setTimeZone(TimeZone.getTimeZone(worldClock.getTimeZoneId()));
+ holder.worldClockCurrentTime.setText(df.format(new Date()));
+ }
+
+ @Override
+ public int getItemCount() {
+ return worldClockList.size();
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ final CardView container;
+
+ final TextView worldClockTimezone;
+ final TextView worldClockLabel;
+ final TextView worldClockCurrentTime;
+
+ ViewHolder(View view) {
+ super(view);
+
+ container = view.findViewById(R.id.card_view);
+
+ worldClockTimezone = view.findViewById(R.id.world_clock_item_timezone);
+ worldClockLabel = view.findViewById(R.id.world_clock_item_label);
+ worldClockCurrentTime = view.findViewById(R.id.world_clock_current_time);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
index 7e97e9862..ef5981eae 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
@@ -62,6 +62,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.TagDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.UserAttributes;
import nodomain.freeyourgadget.gadgetbridge.entities.UserDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.WorldClock;
+import nodomain.freeyourgadget.gadgetbridge.entities.WorldClockDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
@@ -658,6 +660,28 @@ public class DBHelper {
return Collections.emptyList();
}
+ @NonNull
+ public static List getWorldClocks(@NonNull GBDevice gbDevice) {
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ final User user = getUser(daoSession);
+ final Device dbDevice = DBHelper.findDevice(gbDevice, daoSession);
+ if (dbDevice != null) {
+ final WorldClockDao worldClockDao = daoSession.getWorldClockDao();
+ final Long deviceId = dbDevice.getId();
+ final QueryBuilder qb = worldClockDao.queryBuilder();
+ qb.where(
+ WorldClockDao.Properties.UserId.eq(user.getId()),
+ WorldClockDao.Properties.DeviceId.eq(deviceId)).orderAsc(WorldClockDao.Properties.WorldClockId);
+ return qb.build().list();
+ }
+ } catch (final Exception e) {
+ LOG.error("Error reading world clocks from db", e);
+ }
+
+ return Collections.emptyList();
+ }
+
public static void store(final Reminder reminder) {
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession daoSession = db.getDaoSession();
@@ -667,6 +691,15 @@ public class DBHelper {
}
}
+ public static void store(final WorldClock worldClock) {
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ daoSession.insertOrReplace(worldClock);
+ } catch (final Exception e) {
+ LOG.error("Error acquiring database", e);
+ }
+ }
+
public static void delete(final Reminder reminder) {
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession daoSession = db.getDaoSession();
@@ -676,6 +709,15 @@ public class DBHelper {
}
}
+ public static void delete(final WorldClock worldClock) {
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ daoSession.delete(worldClock);
+ } catch (final Exception e) {
+ LOG.error("Error acquiring database", e);
+ }
+ }
+
public static void clearSession() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
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 232c69628..f97a9608a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
@@ -232,6 +232,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return 0;
}
+ @Override
+ public int getWorldClocksSlotCount() {
+ return 0;
+ }
+
+ @Override
+ public int getWorldClocksLabelLength() {
+ return 10;
+ }
+
@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 b329c7970..f5dc931bc 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
@@ -331,6 +331,16 @@ public interface DeviceCoordinator {
*/
int getReminderSlotCount();
+ /**
+ * Indicates the maximum number of slots available for world clocks in the device.
+ */
+ int getWorldClocksSlotCount();
+
+ /**
+ * Indicates the maximum label length for a world clock in the device.
+ */
+ int getWorldClocksLabelLength();
+
/**
* Indicates whether the device has an led which supports custom colors
*/
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
index d20ab54e3..d4ccc7c67 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
@@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
/**
* Specifies all events that Gadgetbridge intends to send to the gadget device.
@@ -49,6 +50,8 @@ public interface EventHandler {
void onSetReminders(ArrayList extends Reminder> reminders);
+ void onSetWorldClocks(ArrayList extends WorldClock> clocks);
+
void onSetCallState(CallSpec callSpec);
void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java
index 75b2061ee..8bf08ac85 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband5/MiBand5Coordinator.java
@@ -85,6 +85,16 @@ public class MiBand5Coordinator extends HuamiCoordinator {
return true;
}
+ @Override
+ public int getWorldClocksSlotCount() {
+ return 20; // as enforced by Mi Fit
+ }
+
+ @Override
+ public int getWorldClocksLabelLength() {
+ return 30; // at least
+ }
+
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
@@ -93,6 +103,7 @@ public class MiBand5Coordinator extends HuamiCoordinator {
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_dateformat,
+ R.xml.devicesettings_world_clocks,
R.xml.devicesettings_nightmode,
R.xml.devicesettings_liftwrist_display_sensitivity,
R.xml.devicesettings_swipeunlock,
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
index 69ede37da..54f57b170 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
@@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.RtlUtils;
@@ -230,6 +231,13 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
+ @Override
+ public void onSetWorldClocks(ArrayList extends WorldClock> clocks) {
+ Intent intent = createIntent().setAction(ACTION_SET_WORLD_CLOCKS)
+ .putExtra(EXTRA_WORLD_CLOCKS, clocks);
+ invokeService(intent);
+ }
+
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
Intent intent = createIntent().setAction(ACTION_SETMUSICINFO)
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
index 8304b8410..ddf52dc7f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
@@ -56,6 +56,7 @@ public interface DeviceService extends EventHandler {
String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms";
String ACTION_SAVE_ALARMS = PREFIX + ".action.save_alarms";
String ACTION_SET_REMINDERS = PREFIX + ".action.set_reminders";
+ String ACTION_SET_WORLD_CLOCKS = PREFIX + ".action.set_world_clocks";
String ACTION_ENABLE_REALTIME_STEPS = PREFIX + ".action.enable_realtime_steps";
String ACTION_REALTIME_SAMPLES = PREFIX + ".action.realtime_samples";
String ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT = PREFIX + ".action.realtime_hr_measurement";
@@ -110,6 +111,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_CONFIG = "config";
String EXTRA_ALARMS = "alarms";
String EXTRA_REMINDERS = "reminders";
+ String EXTRA_WORLD_CLOCKS = "world_clocks";
String EXTRA_CONNECT_FIRST_TIME = "connect_first_time";
String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps";
String EXTRA_INTERVAL_SECONDS = "interval_seconds";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java
new file mode 100644
index 000000000..1c6b0bd1d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/WorldClock.java
@@ -0,0 +1,30 @@
+/* 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.model;
+
+import java.io.Serializable;
+
+public interface WorldClock extends Serializable {
+ /**
+ * The {@link android.os.Bundle} name for transferring parceled world clocks.
+ */
+ String EXTRA_WORLD_CLOCK = "world_clock";
+
+ String getWorldClockId();
+ String getLabel();
+ String getTimeZoneId();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
index a9f7ddfa6..52df51f46 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
@@ -75,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.AutoConnectIntervalReceiver;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.GBAutoFetchReceiver;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
@@ -120,6 +121,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LED_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_REMINDERS;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_WORLD_CLOCKS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION;
@@ -178,6 +180,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RES
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_VIBRATION_INTENSITY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WORLD_CLOCKS;
public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class);
@@ -586,6 +589,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
ArrayList extends Reminder> reminders = (ArrayList extends Reminder>) intent.getSerializableExtra(EXTRA_REMINDERS);
mDeviceSupport.onSetReminders(reminders);
break;
+ case ACTION_SET_WORLD_CLOCKS:
+ ArrayList extends WorldClock> clocks = (ArrayList extends WorldClock>) intent.getSerializableExtra(EXTRA_WORLD_CLOCKS);
+ mDeviceSupport.onSetWorldClocks(clocks);
+ break;
case ACTION_ENABLE_REALTIME_STEPS: {
boolean enable = intent.getBooleanExtra(EXTRA_BOOLEAN_ENABLE, false);
mDeviceSupport.onEnableRealtimeSteps(enable);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
index 0cc8a6d06..bc946df64 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
@@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
/**
* Wraps another device support instance and supports busy-checking and throttling of events.
@@ -318,6 +319,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onSetReminders(reminders);
}
+ @Override
+ public void onSetWorldClocks(ArrayList extends WorldClock> clocks) {
+ if (checkBusy("set world clocks")) {
+ return;
+ }
+ delegate.onSetWorldClocks(clocks);
+ }
+
@Override
public void onEnableRealtimeSteps(boolean enable) {
if (checkBusy("enable realtime steps: " + enable)) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
index 73a9e1b95..9959cc958 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
@@ -38,6 +38,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
@@ -378,6 +379,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
}
+ @Override
+ public void onSetWorldClocks(ArrayList extends WorldClock> clocks) {
+
+ }
+
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
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 665c34f5a..dbf67a768 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
@@ -37,10 +37,16 @@ import net.e175.klaus.solarpositioning.SPA;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.threeten.bp.Instant;
+import org.threeten.bp.ZoneId;
+import org.threeten.bp.zone.ZoneOffsetTransition;
+import org.threeten.bp.zone.ZoneRules;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -52,6 +58,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SimpleTimeZone;
+import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
@@ -112,6 +119,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
@@ -925,6 +933,120 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
writeToChunked(builder, 2, buf.array());
}
+ @Override
+ public void onSetWorldClocks(ArrayList extends WorldClock> clocks) {
+ final TransactionBuilder builder;
+ try {
+ builder = performInitialized("onSetWorldClocks");
+ } catch (final IOException e) {
+ LOG.error("Unable to send world clocks to device", e);
+ return;
+ }
+
+ sendWorldClocks(builder, clocks);
+
+ builder.queue(getQueue());
+ }
+
+ private void setWorldClocks(final TransactionBuilder builder) {
+ final List extends WorldClock> clocks = DBHelper.getWorldClocks(gbDevice);
+ sendWorldClocks(builder, clocks);
+ }
+
+ private void sendWorldClocks(final TransactionBuilder builder, final List extends WorldClock> clocks) {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+ if (coordinator.getWorldClocksSlotCount() == 0) {
+ return;
+ }
+
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ try {
+ baos.write(0x03);
+
+ if (clocks.size() != 0) {
+ int i = clocks.size();
+ for (final WorldClock clock : clocks) {
+ baos.write(i--);
+ baos.write(encodeWorldClock(clock));
+ }
+ } else {
+ baos.write(0);
+ }
+ } catch (final IOException e) {
+ LOG.error("Unable to send world clocks to device", e);
+ return;
+ }
+
+ writeToChunked2021(builder, (short) 0x0008, getNextHandle(), baos.toByteArray(), false, false);
+ }
+
+ public byte[] encodeWorldClock(final WorldClock clock) {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+
+ try {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ 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
+ baos.write(" ".getBytes(StandardCharsets.UTF_8));
+ baos.write(0x00);
+
+ // Some other string? Seems to be empty
+ baos.write(0x00);
+
+ // The city name / label that shows up on the band
+ baos.write(StringUtils.truncate(clock.getLabel(), coordinator.getWorldClocksLabelLength()).getBytes(StandardCharsets.UTF_8));
+ baos.write(0x00);
+
+ // The raw offset from UTC, in number of 15-minute blocks
+ baos.write((int) (timezone.getRawOffset() / (1000L * 60L * 15L)));
+
+ // Daylight savings
+ final boolean useDaylightTime = timezone.useDaylightTime();
+ final boolean inDaylightTime = timezone.inDaylightTime(new Date());
+ byte daylightByte = 0;
+ // The daylight savings offset, either currently (the previous transition) or future (the next transition), in minutes
+ byte daylightOffsetMinutes = 0;
+
+ final ZoneRules zoneRules = zoneId.getRules();
+ if (useDaylightTime) {
+ final ZoneOffsetTransition transition;
+ if (inDaylightTime) {
+ daylightByte = 0x01;
+ transition = zoneRules.previousTransition(Instant.now());
+ } else {
+ daylightByte = 0x02;
+ transition = zoneRules.nextTransition(Instant.now());
+ }
+ daylightOffsetMinutes = (byte) transition.getDuration().toMinutes();
+ }
+
+ baos.write(daylightByte);
+ baos.write(daylightOffsetMinutes);
+
+ // The timestamp of the next daylight savings transition, if any
+ final ZoneOffsetTransition nextTransition = zoneRules.nextTransition(Instant.now());
+ long nextTransitionTs = 0;
+ if (nextTransition != null) {
+ nextTransitionTs = nextTransition
+ .getDateTimeBefore()
+ .atZone(zoneId)
+ .toEpochSecond();
+ }
+
+ for (int i = 0; i < 4; i++) {
+ baos.write((byte) ((nextTransitionTs >> (i * 8)) & 0xff));
+ }
+
+ return baos.toByteArray();
+ } catch (final IOException e) {
+ throw new RuntimeException("This should never happen", e);
+ }
+ }
+
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) {
@@ -3351,6 +3473,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
setExposeHRThridParty(builder);
setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval());
sendReminders(builder);
+ setWorldClocks(builder);
requestAlarms(builder);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java
index 39da5fb9e..d847031d0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java
@@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
/**
@@ -276,4 +277,10 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
byte[] bytes = gbDeviceProtocol.encodeReminders(reminders);
sendToDevice(bytes);
}
+
+ @Override
+ public void onSetWorldClocks(ArrayList extends WorldClock> clocks) {
+ byte[] bytes = gbDeviceProtocol.encodeWorldClocks(clocks);
+ sendToDevice(bytes);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java
index a4408d053..888d64756 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java
@@ -27,6 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
public abstract class GBDeviceProtocol {
@@ -151,6 +152,10 @@ public abstract class GBDeviceProtocol {
return null;
}
+ public byte[] encodeWorldClocks(ArrayList extends WorldClock> clocks) {
+ return null;
+ }
+
public byte[] encodeFmFrequency(float frequency) {
return null;
}
diff --git a/app/src/main/res/layout/activity_configure_world_clocks.xml b/app/src/main/res/layout/activity_configure_world_clocks.xml
new file mode 100644
index 000000000..727f8c4df
--- /dev/null
+++ b/app/src/main/res/layout/activity_configure_world_clocks.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_world_clock_details.xml b/app/src/main/res/layout/activity_world_clock_details.xml
new file mode 100644
index 000000000..93bc99fd6
--- /dev/null
+++ b/app/src/main/res/layout/activity_world_clock_details.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/device_itemv2.xml b/app/src/main/res/layout/device_itemv2.xml
index 98ff82cad..ef17481b9 100644
--- a/app/src/main/res/layout/device_itemv2.xml
+++ b/app/src/main/res/layout/device_itemv2.xml
@@ -73,15 +73,15 @@
android:id="@+id/device_image"
android:layout_width="48dp"
android:layout_height="48dp"
- android:layout_alignParentStart="true"
android:layout_below="@id/device_item_infos_box"
- android:contentDescription="@string/candidate_item_device_image"
- android:clickable="true"
- android:longClickable="true"
- android:background="?android:attr/selectableItemBackground"
- card_view:srcCompat="@drawable/ic_device_pebble"
+ android:layout_alignParentStart="true"
android:layout_marginTop="2dp"
- android:focusable="true" />
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:contentDescription="@string/candidate_item_device_image"
+ android:focusable="true"
+ android:longClickable="true"
+ card_view:srcCompat="@drawable/ic_device_pebble" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8ec87947b..a2422f918 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -486,6 +486,8 @@
Activity and Sleep
Configure alarms
Configure reminders
+ World Clocks
+ Configure clocks for other timezones
Configure alarms
Configure reminders
Repeat
@@ -506,8 +508,15 @@
Are you sure you want to delete the reminder?
No free slots
The device has no free slots for reminders (total slots: %1$s)
+ Delete \'%1$s\'
+ 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)
+ Time Zone
+ Label
Alarm details
Reminder details
+ World Clock details
Sun
Mon
Tue
diff --git a/app/src/main/res/xml/devicesettings_world_clocks.xml b/app/src/main/res/xml/devicesettings_world_clocks.xml
new file mode 100644
index 000000000..46d224761
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_world_clocks.xml
@@ -0,0 +1,8 @@
+
+
+
+