Zepp OS: Manage contacts on watch

This commit is contained in:
José Rebelo 2023-05-17 23:43:29 +01:00
parent f68e4c865b
commit 2b6a79f462
24 changed files with 782 additions and 8 deletions

View File

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

View File

@ -518,6 +518,10 @@
android:name=".activities.ConfigureReminders"
android:label="@string/title_activity_set_reminders"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.ConfigureContacts"
android:label="@string/title_activity_set_contacts"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.ConfigureWorldClocks"
android:label="@string/pref_world_clocks_title"
@ -537,6 +541,12 @@
android:parentActivityName=".activities.ConfigureReminders"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.ContactDetails"
android:label="@string/title_activity_contact_details"
android:parentActivityName=".activities.ConfigureReminders"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.WorldClockDetails"
android:label="@string/title_activity_world_clock_details"

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<GBContactListAdapter.ViewHolder> {
private final Context mContext;
private ArrayList<Contact> contactList;
public GBContactListAdapter(Context context) {
this.mContext = context;
}
public void setContactList(List<Contact> contacts) {
this.contactList = new ArrayList<>(contacts);
}
public ArrayList<Contact> 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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"
android:fitsSystemWindows="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ConfigureContacts">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contact_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:divider="@null" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_add" />
</RelativeLayout>

View File

@ -0,0 +1,96 @@
<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.ContactDetails">
<androidx.cardview.widget.CardView
android:id="@+id/card_contact_name"
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_contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/contact_name"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/contact_name"
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_contact_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_contact_number"
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_contact_name"
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_contact_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/contact_phone_number"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/contact_number"
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_contact_number" />
</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

@ -0,0 +1,51 @@
<?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_contact"
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/contact_item_name"
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="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="4dp"
android:text="John Smith"
android:textAppearance="?android:attr/textAppearance" />
<TextView
android:id="@+id/contact_item_phone_number"
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="0dp"
android:text="+1 000 000 000"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -668,8 +668,11 @@
<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="title_activity_set_contacts">Configure contacts</string>
<string name="pref_world_clocks_title">World Clocks</string>
<string name="pref_world_clocks_summary">Configure clocks for other timezones</string>
<string name="pref_contacts_title">Contacts</string>
<string name="pref_contacts_summary">Configure contacts on the watch</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>
@ -690,6 +693,9 @@
<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="contact_delete_confirm_title">Delete contact</string>
<string name="contact_delete_confirm_description">Are you sure you want to delete \'%1$s\'?</string>
<string name="contact_no_free_slots_description">The device has no free slots for contacts (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>
@ -700,6 +706,7 @@
<string name="world_clock_code">Code</string>
<string name="title_activity_alarm_details">Alarm details</string>
<string name="title_activity_reminder_details">Reminder details</string>
<string name="title_activity_contact_details">Contact 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>
@ -2100,4 +2107,8 @@
<string name="fossil_hr_confirmation_timeout">Confirmation timeout, continuing</string>
<string name="debug_companion_show_associated">Show associated companion devices</string>
<string name="debug_companion_pair_current">Pair current device as companion</string>
<string name="contact_name">Name</string>
<string name="contact_phone_number">Phone number</string>
<string name="contact_missing_name">Contact name is empty</string>
<string name="contact_missing_number">Contact number is empty</string>
</resources>

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_person"
android:key="pref_contacts"
android:summary="@string/pref_contacts_summary"
android:title="@string/pref_contacts_title" />
</androidx.preference.PreferenceScreen>