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 clocks); + void onSetContacts(ArrayList 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 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 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 clocks = (ArrayList) intent.getSerializableExtra(EXTRA_WORLD_CLOCKS); deviceSupport.onSetWorldClocks(clocks); break; + case ACTION_SET_CONTACTS: + ArrayList contacts = (ArrayList) 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 contacts) { + if (checkBusy("set contacts")) { + return; + } + delegate.onSetContacts(contacts); + } + @Override public void onSetWorldClocks(ArrayList 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 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 @@ + + + +