diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
index 31fcbaba1..633b800d7 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(45, MAIN_PACKAGE + ".entities");
+ final Schema schema = new Schema(46, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@@ -89,6 +89,7 @@ public class GBDaoGenerator {
addAlarms(schema, user, device);
addReminders(schema, user, device);
addWorldClocks(schema, user, device);
+ addContacts(schema, user, device);
Entity notificationFilter = addNotificationFilters(schema);
@@ -598,6 +599,24 @@ public class GBDaoGenerator {
worldClock.addToOne(device, deviceId);
}
+ private static void addContacts(Schema schema, Entity user, Entity device) {
+ Entity contact = addEntity(schema, "Contact");
+ contact.implementsInterface("nodomain.freeyourgadget.gadgetbridge.model.Contact");
+ Property deviceId = contact.addLongProperty("deviceId").notNull().getProperty();
+ Property userId = contact.addLongProperty("userId").notNull().getProperty();
+ Property contactId = contact.addStringProperty("contactId").notNull().primaryKey().getProperty();
+ Index indexUnique = new Index();
+ indexUnique.addProperty(deviceId);
+ indexUnique.addProperty(userId);
+ indexUnique.addProperty(contactId);
+ indexUnique.makeUnique();
+ contact.addIndex(indexUnique);
+ contact.addStringProperty("name").notNull();
+ contact.addStringProperty("number").notNull();
+ contact.addToOne(user, userId);
+ contact.addToOne(device, deviceId);
+ }
+
private static void addNotificationFilterEntry(Schema schema, Entity notificationFilterEntity) {
Entity notificatonFilterEntry = addEntity(schema, "NotificationFilterEntry");
notificatonFilterEntry.addIdProperty().autoincrement();
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6f295a40f..d7f79c804 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -518,6 +518,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.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+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.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.adapter.GBContactListAdapter;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.Contact;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+
+
+public class ConfigureContacts extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(ConfigureContacts.class);
+
+ private static final int REQ_CONFIGURE_CONTACT = 1;
+
+ private GBContactListAdapter mGBContactListAdapter;
+ private GBDevice gbDevice;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_configure_contacts);
+
+ gbDevice = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+
+ mGBContactListAdapter = new GBContactListAdapter(this);
+
+ final RecyclerView contactsRecyclerView = findViewById(R.id.contact_list);
+ contactsRecyclerView.setHasFixedSize(true);
+ contactsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ contactsRecyclerView.setAdapter(mGBContactListAdapter);
+ updateContactsFromDB();
+
+ final FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(v -> {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+
+ int deviceSlots = coordinator.getContactsSlotCount(gbDevice);
+
+ if (mGBContactListAdapter.getItemCount() >= deviceSlots) {
+ // No more free slots
+ new AlertDialog.Builder(v.getContext())
+ .setTitle(R.string.reminder_no_free_slots_title)
+ .setMessage(getBaseContext().getString(R.string.contact_no_free_slots_description, String.format(Locale.getDefault(), "%d", deviceSlots)))
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ })
+ .show();
+ return;
+ }
+
+ final Contact contact;
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ final Device device = DBHelper.getDevice(gbDevice, daoSession);
+ final User user = DBHelper.getUser(daoSession);
+ contact = createDefaultContact(device, user);
+ } catch (final Exception e) {
+ LOG.error("Error accessing database", e);
+ return;
+ }
+
+ configureContact(contact);
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQ_CONFIGURE_CONTACT && resultCode == 1) {
+ updateContactsFromDB();
+ sendContactsToDevice();
+ }
+ }
+
+ private Contact createDefaultContact(@NonNull Device device, @NonNull User user) {
+ final Contact contact = new Contact();
+ contact.setName("");
+ contact.setNumber("");
+ contact.setDeviceId(device.getId());
+ contact.setUserId(user.getId());
+ contact.setContactId(UUID.randomUUID().toString());
+
+ return contact;
+ }
+
+ /**
+ * Reads the available contacts from the database and updates the view afterwards.
+ */
+ private void updateContactsFromDB() {
+ final List contacts = DBHelper.getContacts(gbDevice);
+
+ mGBContactListAdapter.setContactList(contacts);
+ mGBContactListAdapter.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 configureContact(final Contact contact) {
+ final Intent startIntent = new Intent(getApplicationContext(), ContactDetails.class);
+ startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice);
+ startIntent.putExtra(Contact.EXTRA_CONTACT, contact);
+ startActivityForResult(startIntent, REQ_CONFIGURE_CONTACT);
+ }
+
+ public void deleteContact(final Contact contact) {
+ DBHelper.delete(contact);
+ updateContactsFromDB();
+ sendContactsToDevice();
+ }
+
+ private void sendContactsToDevice() {
+ if (gbDevice.isInitialized()) {
+ GBApplication.deviceService(gbDevice).onSetContacts(mGBContactListAdapter.getContactList());
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ContactDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ContactDetails.java
new file mode 100644
index 000000000..c4f477136
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ContactDetails.java
@@ -0,0 +1,153 @@
+/* Copyright (C) 2023 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.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.MenuItem;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.Contact;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class ContactDetails extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(ContactDetails.class);
+
+ private Contact contact;
+ private GBDevice device;
+
+ EditText contactName;
+ EditText contactNumber;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_contact_details);
+
+ contact = (Contact) getIntent().getSerializableExtra(Contact.EXTRA_CONTACT);
+
+ if (contact == null) {
+ GB.toast("No contact provided to ContactDetails Activity", Toast.LENGTH_LONG, GB.ERROR);
+ finish();
+ return;
+ }
+
+ contactName = findViewById(R.id.contact_name);
+ contactNumber = findViewById(R.id.contact_number);
+
+ device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
+
+ contactName.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) {
+ contact.setName(s.toString());
+ }
+ });
+
+ contactNumber.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) {
+ contact.setNumber(s.toString());
+ }
+ });
+
+ final FloatingActionButton fab = findViewById(R.id.fab_save);
+ fab.setOnClickListener(view -> {
+ if (StringUtils.isNullOrEmpty(contact.getName())) {
+ GB.toast(getBaseContext().getString(R.string.contact_missing_name), Toast.LENGTH_LONG, GB.WARN);
+ return;
+ }
+
+ if (StringUtils.isNullOrEmpty(contact.getNumber())) {
+ GB.toast(getBaseContext().getString(R.string.contact_missing_number), Toast.LENGTH_LONG, GB.WARN);
+ return;
+ }
+
+ updateContact();
+ ContactDetails.this.setResult(1);
+ finish();
+ });
+
+ updateUiFromContact();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final 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 updateContact() {
+ DBHelper.store(contact);
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull final Bundle state) {
+ super.onSaveInstanceState(state);
+ state.putSerializable("contact", contact);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ contact = (Contact) savedInstanceState.getSerializable("contact");
+ updateUiFromContact();
+ }
+
+ public void updateUiFromContact() {
+ contactName.setText(contact.getName());
+ contactNumber.setText(contact.getNumber());
+ }
+}
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 8e992cebe..34910b171 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
@@ -223,6 +223,7 @@ public class DeviceSettingsPreferenceConst {
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_CONTACTS = "pref_contacts";
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
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 41c0a5f79..908328f27 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
@@ -50,6 +50,7 @@ import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.CalBlacklistActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureContacts;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
@@ -755,6 +756,19 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
});
}
+ final Preference contacts = findPreference(PREF_CONTACTS);
+ if (contacts != null) {
+ contacts.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final Intent intent = new Intent(getContext(), ConfigureContacts.class);
+ intent.putExtra(GBDevice.EXTRA_DEVICE, device);
+ startActivity(intent);
+ return true;
+ }
+ });
+ }
+
final Preference calendarBlacklist = findPreference("blacklist_calendars");
if (calendarBlacklist != null) {
calendarBlacklist.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBContactListAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBContactListAdapter.java
new file mode 100644
index 000000000..5084e4c83
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBContactListAdapter.java
@@ -0,0 +1,109 @@
+/* Copyright (C) 2023 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.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.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureContacts;
+import nodomain.freeyourgadget.gadgetbridge.entities.Contact;
+
+/**
+ * Adapter for displaying Contact instances.
+ */
+public class GBContactListAdapter extends RecyclerView.Adapter {
+
+ private final Context mContext;
+ private ArrayList contactList;
+
+ public GBContactListAdapter(Context context) {
+ this.mContext = context;
+ }
+
+ public void setContactList(List contacts) {
+ this.contactList = new ArrayList<>(contacts);
+ }
+
+ public ArrayList getContactList() {
+ return contactList;
+ }
+
+ @NonNull
+ @Override
+ public GBContactListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
+ final Contact contact = contactList.get(position);
+
+ holder.container.setOnClickListener(v -> ((ConfigureContacts) mContext).configureContact(contact));
+
+ holder.container.setOnLongClickListener(v -> {
+ new AlertDialog.Builder(v.getContext())
+ .setTitle(R.string.contact_delete_confirm_title)
+ .setMessage(mContext.getString(R.string.contact_delete_confirm_description, contact.getName()))
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
+ ((ConfigureContacts) mContext).deleteContact(contact);
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+
+ return true;
+ });
+
+ holder.contactName.setText(contact.getName());
+ holder.contactNumber.setText(contact.getNumber());
+ }
+
+ @Override
+ public int getItemCount() {
+ return contactList.size();
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ final CardView container;
+
+ final TextView contactName;
+ final TextView contactNumber;
+
+ ViewHolder(View view) {
+ super(view);
+
+ container = view.findViewById(R.id.card_contact);
+
+ contactName = view.findViewById(R.id.contact_item_name);
+ contactNumber = view.findViewById(R.id.contact_item_phone_number);
+ }
+ }
+}
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 13992c094..86061ccf4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java
@@ -50,6 +50,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescription;
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescriptionDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.Contact;
+import nodomain.freeyourgadget.gadgetbridge.entities.ContactDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes;
@@ -668,6 +670,28 @@ public class DBHelper {
return Collections.emptyList();
}
+ @NonNull
+ public static List getContacts(@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 ContactDao contactDao = daoSession.getContactDao();
+ final Long deviceId = dbDevice.getId();
+ final QueryBuilder qb = contactDao.queryBuilder();
+ qb.where(
+ ContactDao.Properties.UserId.eq(user.getId()),
+ ContactDao.Properties.DeviceId.eq(deviceId)).orderAsc(ContactDao.Properties.Name);
+ return qb.build().list();
+ }
+ } catch (final Exception e) {
+ LOG.error("Error reading contacts from db", e);
+ }
+
+ return Collections.emptyList();
+ }
+
public static void store(final Reminder reminder) {
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession daoSession = db.getDaoSession();
@@ -686,6 +710,15 @@ public class DBHelper {
}
}
+ public static void store(final Contact contact) {
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ daoSession.insertOrReplace(contact);
+ } 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();
@@ -704,6 +737,15 @@ public class DBHelper {
}
}
+ public static void delete(final Contact contact) {
+ try (DBHandler db = GBApplication.acquireDB()) {
+ final DaoSession daoSession = db.getDaoSession();
+ daoSession.delete(contact);
+ } 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 8fd90bacc..8045f84c6 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
@@ -269,6 +269,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
+ @Override
+ public int getContactsSlotCount(final GBDevice device) {
+ return 0;
+ }
+
@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 8704eb224..404af7a30 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
@@ -403,6 +403,11 @@ public interface DeviceCoordinator {
*/
boolean supportsDisabledWorldClocks();
+ /**
+ * Indicates the maximum number of slots available for contacts in the device.
+ */
+ int getContactsSlotCount(GBDevice device);
+
/**
* 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 9dcefae0e..407500c11 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java
@@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -53,6 +54,8 @@ public interface EventHandler {
void onSetWorldClocks(ArrayList extends WorldClock> clocks);
+ void onSetContacts(ArrayList extends Contact> contacts);
+
void onSetCallState(CallSpec callSpec);
void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java
index 1ade75964..fbc1a47b5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
@@ -171,6 +172,11 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
return getPrefs(device).getInt(Huami2021Service.REMINDERS_PREF_CAPABILITY, 0);
}
+ @Override
+ public int getContactsSlotCount(final GBDevice device) {
+ return getPrefs(device).getInt(ZeppOsContactsService.PREF_CONTACTS_SLOT_COUNT, 0);
+ }
+
@Override
public String[] getSupportedLanguageSettings(final GBDevice device) {
// Return all known languages by default. Unsupported languages will be removed by Huami2021SettingsCustomizer
@@ -280,6 +286,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
// Other
//
settings.add(R.xml.devicesettings_header_other);
+ if (getContactsSlotCount(device) > 0) {
+ settings.add(R.xml.devicesettings_contacts);
+ }
settings.add(R.xml.devicesettings_offline_voice);
settings.add(R.xml.devicesettings_device_actions_without_not_wear);
settings.add(R.xml.devicesettings_buttonactions_upper_long);
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 415fa889d..c695da5c7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java
@@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
@@ -268,6 +269,13 @@ public class GBDeviceService implements DeviceService {
invokeService(intent);
}
+ @Override
+ public void onSetContacts(ArrayList extends Contact> contacts) {
+ Intent intent = createIntent().setAction(ACTION_SET_CONTACTS)
+ .putExtra(EXTRA_CONTACTS, contacts);
+ 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 aa3452f87..7ad85ad4b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java
@@ -60,6 +60,7 @@ public interface DeviceService extends EventHandler {
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_SET_CONTACTS = PREFIX + ".action.set_contacts";
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";
@@ -118,6 +119,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_ALARMS = "alarms";
String EXTRA_REMINDERS = "reminders";
String EXTRA_WORLD_CLOCKS = "world_clocks";
+ String EXTRA_CONTACTS = "contacts";
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/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
index 2f6336a59..05a8ce8ac 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java
@@ -84,6 +84,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -639,6 +640,16 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
}
+ /**
+ * If contacts can be configured on the device, this method can be
+ * overridden and implemented by the device support class.
+ * @param contacts {@link java.util.ArrayList} containing {@link nodomain.freeyourgadget.gadgetbridge.model.Contact} instances
+ */
+ @Override
+ public void onSetContacts(ArrayList extends Contact> contacts) {
+
+ }
+
/**
* If the device can receive and display notifications, this method
* can be overridden and implemented by the device support class.
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 23bf7e921..ff307a247 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java
@@ -77,6 +77,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -126,6 +127,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONTACTS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_FM_FREQUENCY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_GPS_LOCATION;
@@ -161,6 +163,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAN
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONNECT_FIRST_TIME;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONTACTS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FM_FREQUENCY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_GPS_LOCATION;
@@ -918,6 +921,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
ArrayList extends WorldClock> clocks = (ArrayList extends WorldClock>) intent.getSerializableExtra(EXTRA_WORLD_CLOCKS);
deviceSupport.onSetWorldClocks(clocks);
break;
+ case ACTION_SET_CONTACTS:
+ ArrayList extends Contact> contacts = (ArrayList extends Contact>) intent.getSerializableExtra(EXTRA_CONTACTS);
+ deviceSupport.onSetContacts(contacts);
+ break;
case ACTION_ENABLE_REALTIME_STEPS: {
boolean enable = intent.getBooleanExtra(EXTRA_BOOLEAN_ENABLE, false);
deviceSupport.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 2fd27ed6f..2ebd32184 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java
@@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -355,6 +356,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
delegate.onSetReminders(reminders);
}
+ @Override
+ public void onSetContacts(ArrayList extends Contact> contacts) {
+ if (checkBusy("set contacts")) {
+ return;
+ }
+ delegate.onSetContacts(contacts);
+ }
+
@Override
public void onSetWorldClocks(ArrayList extends WorldClock> clocks) {
if (checkBusy("set world clocks")) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java
index e3349adb6..765e63c0f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java
@@ -91,6 +91,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -595,6 +596,11 @@ public abstract class Huami2021Support extends HuamiSupport {
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_REMINDERS, buf.array(), false);
}
+ @Override
+ public void onSetContacts(ArrayList extends Contact> contacts) {
+ contactsService.setContacts((List) contacts);
+ }
+
@Override
protected boolean isWorldClocksEncrypted() {
return true;
@@ -1182,7 +1188,6 @@ public abstract class Huami2021Support extends HuamiSupport {
phoneService.requestCapabilities(builder);
phoneService.requestEnabled(builder);
}
- //contactsService.requestCapabilities(builder);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsContactsService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsContactsService.java
index 0c31f11ed..c6860247f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsContactsService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsContactsService.java
@@ -23,6 +23,7 @@ import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
@@ -43,6 +44,8 @@ public class ZeppOsContactsService extends AbstractZeppOsService {
private int version = 0;
private int maxContacts = 0;
+ public static final String PREF_CONTACTS_SLOT_COUNT = "zepp_os_contacts_slot_count";
+
public ZeppOsContactsService(final Huami2021Support support) {
super(support);
}
@@ -68,6 +71,7 @@ public class ZeppOsContactsService extends AbstractZeppOsService {
}
maxContacts = BLETypeConversions.toUint16(payload, 2);
LOG.info("Contacts version={}, maxContacts={}", version, maxContacts);
+ getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_CONTACTS_SLOT_COUNT, maxContacts));
break;
case CMD_SET_LIST_ACK:
LOG.info("Got contacts set list ack, status = {}", payload[1]);
@@ -77,12 +81,9 @@ public class ZeppOsContactsService extends AbstractZeppOsService {
}
}
- public int maxContacts() {
- return maxContacts;
- }
-
- public boolean isSupported() {
- return version == 1 && maxContacts != 0;
+ @Override
+ public void initialize(final TransactionBuilder builder) {
+ requestCapabilities(builder);
}
public void requestCapabilities(final TransactionBuilder builder) {
diff --git a/app/src/main/res/layout/activity_configure_contacts.xml b/app/src/main/res/layout/activity_configure_contacts.xml
new file mode 100644
index 000000000..740bed323
--- /dev/null
+++ b/app/src/main/res/layout/activity_configure_contacts.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_contact_details.xml b/app/src/main/res/layout/activity_contact_details.xml
new file mode 100644
index 000000000..2f5d8e4a8
--- /dev/null
+++ b/app/src/main/res/layout/activity_contact_details.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_contact.xml b/app/src/main/res/layout/item_contact.xml
new file mode 100644
index 000000000..5a283be12
--- /dev/null
+++ b/app/src/main/res/layout/item_contact.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 64edd2915..481312fc6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -668,8 +668,11 @@
Activity and Sleep
Configure alarms
Configure reminders
+ Configure contacts
World Clocks
Configure clocks for other timezones
+ Contacts
+ Configure contacts on the watch
Configure alarms
Configure reminders
Repeat
@@ -690,6 +693,9 @@
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 contact
+ Are you sure you want to delete \'%1$s\'?
+ The device has no free slots for contacts (total slots: %1$s)
Delete \'%1$s\'
Are you sure you want to delete the world clock?
No free slots
@@ -700,6 +706,7 @@
Code
Alarm details
Reminder details
+ Contact details
World Clock details
Sun
Mon
@@ -2100,4 +2107,8 @@
Confirmation timeout, continuing
Show associated companion devices
Pair current device as companion
+ Name
+ Phone number
+ Contact name is empty
+ Contact number is empty
diff --git a/app/src/main/res/xml/devicesettings_contacts.xml b/app/src/main/res/xml/devicesettings_contacts.xml
new file mode 100644
index 000000000..f1688c733
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_contacts.xml
@@ -0,0 +1,8 @@
+
+
+
+