Mi Band 5: Add support for World Clocks

This commit is contained in:
José Rebelo 2022-05-09 19:47:08 +01:00 committed by Gitea
parent d973f50560
commit 5c6edea233
30 changed files with 1100 additions and 41 deletions

View File

@ -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();

View File

@ -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")

View File

@ -498,6 +498,10 @@
android:name=".activities.ConfigureReminders"
android:label="@string/title_activity_set_reminders"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.ConfigureWorldClocks"
android:label="@string/pref_world_clocks_title"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.devicesettings.DeviceSettingsActivity"
android:label="@string/title_activity_device_specific_settings"
@ -513,6 +517,12 @@
android:parentActivityName=".activities.ConfigureReminders"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.WorldClockDetails"
android:label="@string/title_activity_world_clock_details"
android:parentActivityName=".activities.ConfigureWorldClocks"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.VibrationActivity"
android:label="@string/title_activity_vibration"

View File

@ -100,6 +100,8 @@ import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_ID_ERROR
import androidx.multidex.MultiDex;
import com.jakewharton.threetenabp.AndroidThreeTen;
/**
* Main Application class that initializes and provides access to certain things like
* logging and DB access.
@ -191,6 +193,9 @@ public class GBApplication extends Application {
return;
}
// Initialize the timezones library
AndroidThreeTen.init(this);
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs = new Prefs(sharedPrefs);
gbPrefs = new GBPrefs(prefs);

View File

@ -0,0 +1,218 @@
/* 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 <http://www.gnu.org/licenses/>. */
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<WorldClock> 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());
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<String> 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());
}
}

View File

@ -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);

View File

@ -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";

View File

@ -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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<GBWorldClockListAdapter.ViewHolder> {
private final Context mContext;
private ArrayList<WorldClock> worldClockList;
public GBWorldClockListAdapter(final Context context) {
this.mContext = context;
}
public void setWorldClockList(final List<WorldClock> worldClocks) {
this.worldClockList = new ArrayList<>(worldClocks);
}
public ArrayList<WorldClock> 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);
}
}
}

View File

@ -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<WorldClock> 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<WorldClock> 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();

View File

@ -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;

View File

@ -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
*/

View File

@ -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);

View File

@ -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,

View File

@ -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)

View File

@ -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";

View File

@ -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 <http://www.gnu.org/licenses/>. */
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();
}

View File

@ -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);

View File

@ -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)) {

View File

@ -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) {

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,27 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:fitsSystemWindows="true"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:divider="@null"
android:id="@+id/world_clock_list" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_gravity="bottom|end"
app:srcCompat="@drawable/ic_add"
android:layout_margin="16dp" />
</RelativeLayout>

View File

@ -0,0 +1,97 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ReminderDetails">
<androidx.cardview.widget.CardView
android:id="@+id/card_timezone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
card_view:cardCornerRadius="4dp"
card_view:cardElevation="4dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_timezone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/world_clock_timezone"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/world_clock_timezone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="?"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_timezone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_timezone"
card_view:cardCornerRadius="4dp"
card_view:cardElevation="4dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/world_clock_label"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/world_clock_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_message" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_save" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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" />
<TextView
android:id="@+id/device_name"

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardCornerRadius="4dp"
card_view:cardElevation="4dp"
card_view:contentPadding="4dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/world_clock_item_timezone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="3dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="8dp"
android:text="Middle-earth/Gondor"
android:textAppearance="?android:attr/textAppearance" />
<TextView
android:id="@+id/world_clock_item_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="3dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="8dp"
android:text="?"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/world_clock_current_time"
android:layout_width="48dp"
android:layout_height="36dp"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:layout_marginEnd="0dp"
android:layout_marginBottom="8dp"
android:text="00:00"
card_view:tint="@color/secondarytext" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -486,6 +486,8 @@
<string name="title_activity_charts">Activity and Sleep</string>
<string name="title_activity_set_alarm">Configure alarms</string>
<string name="title_activity_set_reminders">Configure reminders</string>
<string name="pref_world_clocks_title">World Clocks</string>
<string name="pref_world_clocks_summary">Configure clocks for other timezones</string>
<string name="controlcenter_start_configure_alarms">Configure alarms</string>
<string name="controlcenter_start_configure_reminders">Configure reminders</string>
<string name="reminder_repeat">Repeat</string>
@ -506,8 +508,15 @@
<string name="reminder_delete_confirm_description">Are you sure you want to delete the reminder?</string>
<string name="reminder_no_free_slots_title">No free slots</string>
<string name="reminder_no_free_slots_description">The device has no free slots for reminders (total slots: %1$s)</string>
<string name="world_clock_delete_confirm_title">Delete \'%1$s\'</string>
<string name="world_clock_delete_confirm_description">Are you sure you want to delete the world clock?</string>
<string name="world_clock_no_free_slots_title">No free slots</string>
<string name="world_clock_no_free_slots_description">The device has no free slots for world clocks (total slots: %1$s)</string>
<string name="world_clock_timezone">Time Zone</string>
<string name="world_clock_label">Label</string>
<string name="title_activity_alarm_details">Alarm details</string>
<string name="title_activity_reminder_details">Reminder details</string>
<string name="title_activity_world_clock_details">World Clock details</string>
<string name="alarm_sun_short">Sun</string>
<string name="alarm_mon_short">Mon</string>
<string name="alarm_tue_short">Tue</string>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_access_time"
android:key="pref_world_clocks"
android:summary="@string/pref_world_clocks_summary"
android:title="@string/pref_world_clocks_title" />
</androidx.preference.PreferenceScreen>