diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 7f9432366..b6f5fdef6 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 { - Schema schema = new Schema(36, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(37, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -86,6 +86,7 @@ public class GBDaoGenerator { addHybridHRActivitySample(schema, user, device); addCalendarSyncState(schema, device); addAlarms(schema, user, device); + addReminders(schema, user, device); Entity notificationFilter = addNotificationFilters(schema); @@ -541,6 +542,25 @@ public class GBDaoGenerator { alarm.addToOne(device, deviceId); } + private static void addReminders(Schema schema, Entity user, Entity device) { + Entity reminder = addEntity(schema, "Reminder"); + reminder.implementsInterface("nodomain.freeyourgadget.gadgetbridge.model.Reminder"); + Property deviceId = reminder.addLongProperty("deviceId").notNull().getProperty(); + Property userId = reminder.addLongProperty("userId").notNull().getProperty(); + Property reminderId = reminder.addStringProperty("reminderId").notNull().primaryKey().getProperty(); + Index indexUnique = new Index(); + indexUnique.addProperty(deviceId); + indexUnique.addProperty(userId); + indexUnique.addProperty(reminderId); + indexUnique.makeUnique(); + reminder.addIndex(indexUnique); + reminder.addStringProperty("message").notNull(); + reminder.addDateProperty("date").notNull(); + reminder.addIntProperty("repetition").notNull(); + reminder.addToOne(user, userId); + reminder.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 cab98550f..4e7b3156d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -485,6 +485,10 @@ android:name=".activities.ConfigureAlarms" android:label="@string/title_activity_set_alarm" android:parentActivityName=".activities.ControlCenterv2" /> + + . */ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +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.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.adapter.GBReminderListAdapter; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.Reminder; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + + +public class ConfigureReminders extends AbstractGBActivity { + private static final Logger LOG = LoggerFactory.getLogger(ConfigureReminders.class); + + private static final int REQ_CONFIGURE_REMINDER = 1; + + private GBReminderListAdapter mGBReminderListAdapter; + private GBDevice gbDevice; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_configure_reminders); + + gbDevice = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE); + + mGBReminderListAdapter = new GBReminderListAdapter(this); + + final RecyclerView remindersRecyclerView = findViewById(R.id.reminder_list); + remindersRecyclerView.setHasFixedSize(true); + remindersRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + remindersRecyclerView.setAdapter(mGBReminderListAdapter); + updateRemindersFromDB(); + + final FloatingActionButton fab = findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); + + final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); + int reservedSlots = prefs.getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, 9); + + int deviceSlots = coordinator.getReminderSlotCount() - reservedSlots; + + if (mGBReminderListAdapter.getItemCount() >= deviceSlots) { + // No more free slots + new AlertDialog.Builder(v.getContext()) + .setTitle(R.string.reminder_no_free_slots_title) + .setMessage(getBaseContext().getString(R.string.reminder_no_free_slots_description, String.format(Locale.getDefault(), "%d", deviceSlots))) + .setIcon(R.drawable.ic_warning) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(final DialogInterface dialog, final int whichButton) { + } + }) + .show(); + return; + } + + final Reminder reminder; + try (DBHandler db = GBApplication.acquireDB()) { + final DaoSession daoSession = db.getDaoSession(); + final Device device = DBHelper.getDevice(gbDevice, daoSession); + final User user = DBHelper.getUser(daoSession); + reminder = createDefaultReminder(device, user); + } catch (final Exception e) { + LOG.error("Error accessing database", e); + return; + } + + configureReminder(reminder); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQ_CONFIGURE_REMINDER && resultCode == 1) { + updateRemindersFromDB(); + sendRemindersToDevice(); + } + } + + private Reminder createDefaultReminder(@NonNull Device device, @NonNull User user) { + final Reminder reminder = new Reminder(); + reminder.setRepetition(Reminder.ONCE); + reminder.setDate(Calendar.getInstance().getTime()); + reminder.setMessage(""); + reminder.setDeviceId(device.getId()); + reminder.setUserId(user.getId()); + reminder.setReminderId(UUID.randomUUID().toString()); + + return reminder; + } + + /** + * Reads the available reminders from the database and updates the view afterwards. + */ + private void updateRemindersFromDB() { + final List reminders = DBHelper.getReminders(gbDevice); + + mGBReminderListAdapter.setReminderList(reminders); + mGBReminderListAdapter.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 configureReminder(final Reminder reminder) { + final Intent startIntent = new Intent(getApplicationContext(), ReminderDetails.class); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice); + startIntent.putExtra(Reminder.EXTRA_REMINDER, reminder); + startActivityForResult(startIntent, REQ_CONFIGURE_REMINDER); + } + + public void deleteReminder(final Reminder reminder) { + DBHelper.delete(reminder); + updateRemindersFromDB(); + sendRemindersToDevice(); + } + + private void sendRemindersToDevice() { + if (gbDevice.isInitialized()) { + GBApplication.deviceService().onSetReminders(mGBReminderListAdapter.getReminderList()); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ReminderDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ReminderDetails.java new file mode 100644 index 000000000..c0a675f97 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ReminderDetails.java @@ -0,0 +1,222 @@ +/* Copyright (C) 2019 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.app.AlertDialog; +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.text.format.DateFormat; +import android.view.MenuItem; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.TimePicker; +import android.widget.Toast; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.Reminder; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class ReminderDetails extends AbstractGBActivity implements TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener { + private static final Logger LOG = LoggerFactory.getLogger(ReminderDetails.class); + + private Reminder reminder; + private GBDevice device; + + ArrayAdapter repeatAdapter; + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault()); + + TextView reminderRepeat; + TextView reminderDate; + TextView reminderTime; + EditText reminderText; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_reminder_details); + + reminder = (Reminder) getIntent().getSerializableExtra(Reminder.EXTRA_REMINDER); + + if (reminder == null) { + GB.toast("No reminder provided to ReminderDetails Activity", Toast.LENGTH_LONG, GB.ERROR); + finish(); + return; + } + + reminderRepeat = findViewById(R.id.reminder_repeat); + reminderDate = findViewById(R.id.reminder_date); + reminderTime = findViewById(R.id.reminder_time); + reminderText = findViewById(R.id.reminder_message); + + device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE); + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); + + final String[] repeatStrings = getResources().getStringArray(R.array.reminder_repeat); + repeatAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, repeatStrings); + + final View cardRepeat = findViewById(R.id.card_repeat); + cardRepeat.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new AlertDialog.Builder(ReminderDetails.this).setAdapter(repeatAdapter, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + reminder.setRepetition(i); + updateUiFromReminder(); + } + }).create().show(); + } + }); + + final View cardDate = findViewById(R.id.card_date); + cardDate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final Calendar date = new GregorianCalendar(); + date.setTime(reminder.getDate()); + new DatePickerDialog( + ReminderDetails.this, + ReminderDetails.this, + date.get(Calendar.YEAR), + date.get(Calendar.MONTH), + date.get(Calendar.DAY_OF_MONTH) + ).show(); + } + }); + + final View cardTime = findViewById(R.id.card_time); + cardTime.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new TimePickerDialog( + ReminderDetails.this, + ReminderDetails.this, + reminder.getDate().getHours(), + reminder.getDate().getMinutes(), + DateFormat.is24HourFormat(GBApplication.getContext()) + ).show(); + } + }); + + reminderText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(coordinator.getMaximumReminderMessageLength())}); + reminderText.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) { + reminder.setMessage(s.toString()); + } + }); + + final FloatingActionButton fab = findViewById(R.id.fab_save); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + updateReminder(); + ReminderDetails.this.setResult(1); + finish(); + } + }); + + updateUiFromReminder(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // back button + // TODO confirm when exiting without saving + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void updateReminder() { + DBHelper.store(reminder); + } + + @Override + protected void onSaveInstanceState(Bundle state) { + super.onSaveInstanceState(state); + state.putSerializable("reminder", reminder); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + reminder = (Reminder) savedInstanceState.getSerializable("reminder"); + updateUiFromReminder(); + } + + @Override + public void onTimeSet(TimePicker timePicker, int hour, int minute) { + reminder.getDate().setHours(hour); + reminder.getDate().setMinutes(minute); + updateUiFromReminder(); + } + + @Override + public void onDateSet(DatePicker datePicker, int year, int month, int dayOfMonth) { + final Calendar date = new GregorianCalendar(year, month, dayOfMonth); + reminder.setDate(new Date(date.getTimeInMillis())); + updateUiFromReminder(); + } + + public void updateUiFromReminder() { + reminderRepeat.setText(repeatAdapter.getItem(reminder.getRepetition())); + reminderText.setText(reminder.getMessage()); + + if (reminder.getDate() != null) { + reminderDate.setText(dateFormat.format(reminder.getDate())); + reminderTime.setText(timeFormat.format(reminder.getDate())); + } + } +} 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 4b4d0235e..72eb79718 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 @@ -25,6 +25,7 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_NOTIFICATION_ENABLE = "notification_enable"; public static final String PREF_SCREEN_ORIENTATION = "screen_orientation"; public static final String PREF_RESERVER_ALARMS_CALENDAR = "reserve_alarms_calendar"; + public static final String PREF_RESERVER_REMINDERS_CALENDAR = "reserve_reminders_calendar"; public static final String PREF_ALLOW_HIGH_MTU = "allow_high_mtu"; public static final String PREF_SYNC_CALENDAR = "sync_calendar"; public static final String PREF_USE_CUSTOM_DEVICEICON = "use_custom_deviceicon"; 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 c36f4ca78..a58047b94 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 @@ -738,6 +738,7 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp setInputTypeFor(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS, InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); setInputTypeFor(MakibesHR3Constants.PREF_FIND_PHONE_DURATION, InputType.TYPE_CLASS_NUMBER); setInputTypeFor(DeviceSettingsPreferenceConst.PREF_RESERVER_ALARMS_CALENDAR, InputType.TYPE_CLASS_NUMBER); + setInputTypeFor(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, InputType.TYPE_CLASS_NUMBER); String deviceActionsFellSleepSelection = prefs.getString(PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION, PREF_DEVICE_ACTION_SELECTION_OFF); final Preference deviceActionsFellSleep = findPreference(PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index 55736a72d..84af915e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -75,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesActivity; import nodomain.freeyourgadget.gadgetbridge.activities.BatteryInfoActivity; import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms; +import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureReminders; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateDialog; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; @@ -320,6 +321,21 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter 0 ? View.VISIBLE : View.GONE); + holder.setRemindersView.setOnClickListener(new View.OnClickListener() + + { + @Override + public void onClick(View v) { + Intent startIntent; + startIntent = new Intent(context, ConfigureReminders.class); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, device); + context.startActivity(startIntent); + } + } + ); + //show graphs holder.showActivityGraphs.setVisibility(coordinator.supportsActivityTracking() ? View.VISIBLE : View.GONE); holder.showActivityGraphs.setOnClickListener(new View.OnClickListener() @@ -743,6 +759,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter. */ +package nodomain.freeyourgadget.gadgetbridge.adapter; + + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureReminders; +import nodomain.freeyourgadget.gadgetbridge.entities.Reminder; + +/** + * Adapter for displaying Reminder instances. + */ +public class GBReminderListAdapter extends RecyclerView.Adapter { + + private final Context mContext; + private ArrayList reminderList; + + public GBReminderListAdapter(Context context) { + this.mContext = context; + } + + public void setReminderList(List reminders) { + this.reminderList = new ArrayList<>(reminders); + } + + public ArrayList getReminderList() { + return reminderList; + } + + @NonNull + @Override + public GBReminderListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_reminder, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + final Reminder reminder = reminderList.get(position); + + holder.container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ((ConfigureReminders) mContext).configureReminder(reminder); + } + }); + + holder.container.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + new AlertDialog.Builder(v.getContext()) + .setTitle(R.string.reminder_delete_confirm_title) + .setMessage(R.string.reminder_delete_confirm_description) + .setIcon(R.drawable.ic_warning) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(final DialogInterface dialog, final int whichButton) { + ((ConfigureReminders) mContext).deleteReminder(reminder); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + + return true; + } + }); + + holder.reminderMessage.setText(reminder.getMessage()); + + final Date time = reminder.getDate(); + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + + int stringResId = 0; + + switch (reminder.getRepetition()) { + case Reminder.ONCE: + stringResId = R.string.reminder_time_once; + break; + case Reminder.EVERY_DAY: + stringResId = R.string.reminder_time_every_day; + break; + case Reminder.EVERY_WEEK: + stringResId = R.string.reminder_time_every_week; + break; + case Reminder.EVERY_MONTH: + stringResId = R.string.reminder_time_every_month; + break; + case Reminder.EVERY_YEAR: + stringResId = R.string.reminder_time_every_year; + break; + } + + final String reminderTimeText = mContext.getString(stringResId, format.format(time)); + holder.reminderTime.setText(reminderTimeText); + } + + @Override + public int getItemCount() { + return reminderList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + final CardView container; + + final TextView reminderTime; + final TextView reminderMessage; + + ViewHolder(View view) { + super(view); + + container = view.findViewById(R.id.card_view); + + reminderTime = view.findViewById(R.id.reminder_item_time); + reminderMessage = view.findViewById(R.id.reminder_item_message); + } + } +} 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 056fffc95..7e97e9862 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java @@ -55,6 +55,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao; +import nodomain.freeyourgadget.gadgetbridge.entities.Reminder; +import nodomain.freeyourgadget.gadgetbridge.entities.ReminderDao; import nodomain.freeyourgadget.gadgetbridge.entities.Tag; import nodomain.freeyourgadget.gadgetbridge.entities.TagDao; import nodomain.freeyourgadget.gadgetbridge.entities.User; @@ -620,6 +622,60 @@ public class DBHelper { } } + /** + * Returns all user-configurable reminders for the given user and device. The list is sorted by + * {@link Reminder#getDate}. Calendar events that may also be modeled as reminders are not stored + * in the database and hence not returned by this method. + * @param gbDevice the device for which the alarms shall be loaded + * @return the list of reminders for the given device + */ + @NonNull + public static List getReminders(@NonNull GBDevice gbDevice) { + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); + final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); + + int reservedSlots = prefs.getInt(DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR, 9); + + final int reminderSlots = coordinator.getReminderSlotCount(); + + 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 ReminderDao reminderDao = daoSession.getReminderDao(); + final Long deviceId = dbDevice.getId(); + final QueryBuilder qb = reminderDao.queryBuilder(); + qb.where( + ReminderDao.Properties.UserId.eq(user.getId()), + ReminderDao.Properties.DeviceId.eq(deviceId)).orderAsc(ReminderDao.Properties.Date).limit(reminderSlots - reservedSlots); + return qb.build().list(); + } + } catch (final Exception e) { + LOG.error("Error reading reminders from db", e); + } + + return Collections.emptyList(); + } + + public static void store(final Reminder reminder) { + try (DBHandler db = GBApplication.acquireDB()) { + final DaoSession daoSession = db.getDaoSession(); + daoSession.insertOrReplace(reminder); + } 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(); + daoSession.delete(reminder); + } 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 3cf0b1e27..e4e8bd43f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -222,6 +222,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public int getMaximumReminderMessageLength() { + return 0; + } + + @Override + public int getReminderSlotCount() { + 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 8dda292c0..d934deadc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -321,6 +321,16 @@ public interface DeviceCoordinator { */ boolean supportsMusicInfo(); + /** + * Indicates the maximum reminder message length. + */ + int getMaximumReminderMessageLength(); + + /** + * Indicates the maximum number of reminder slots available in the device. + */ + int getReminderSlotCount(); + /** * 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 e20493c11..9668d6acb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; /** @@ -46,6 +47,8 @@ public interface EventHandler { void onSetAlarms(ArrayList alarms); + void onSetReminders(ArrayList reminders); + void onSetCallState(CallSpec callSpec); void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java index 050938da5..4b9ac8447 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java @@ -84,6 +84,15 @@ public class MiBand3Coordinator extends HuamiCoordinator { return true; } + @Override + public int getMaximumReminderMessageLength() { + return 16; + } + + @Override + public int getReminderSlotCount() { + return 22; // At least, Mi Fit still allows more + } public static String getNightMode(String deviceAddress) { Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress)); @@ -111,6 +120,7 @@ public class MiBand3Coordinator extends HuamiCoordinator { R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_swipeunlock, R.xml.devicesettings_sync_calendar, + R.xml.devicesettings_reserve_reminders_calendar, R.xml.devicesettings_expose_hr_thirdparty, R.xml.devicesettings_bt_connected_advertisement, R.xml.devicesettings_device_actions, 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 9773147c9..c16f94df1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; import nodomain.freeyourgadget.gadgetbridge.util.RtlUtils; @@ -225,6 +226,13 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onSetReminders(ArrayList reminders) { + Intent intent = createIntent().setAction(ACTION_SET_REMINDERS) + .putExtra(EXTRA_REMINDERS, reminders); + 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 95da94ec9..62f71e9d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -55,6 +55,7 @@ public interface DeviceService extends EventHandler { String ACTION_SET_CONSTANT_VIBRATION = PREFIX + ".action.set_constant_vibration"; String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms"; String ACTION_SAVE_ALARMS = PREFIX + ".action.save_alarms"; + String ACTION_SET_REMINDERS = PREFIX + ".action.set_reminders"; String ACTION_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"; @@ -107,6 +108,7 @@ public interface DeviceService extends EventHandler { String EXTRA_URI = "uri"; String EXTRA_CONFIG = "config"; String EXTRA_ALARMS = "alarms"; + String EXTRA_REMINDERS = "reminders"; String EXTRA_CONNECT_FIRST_TIME = "connect_first_time"; String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps"; String EXTRA_INTERVAL_SECONDS = "interval_seconds"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Reminder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Reminder.java new file mode 100644 index 000000000..278f26cde --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Reminder.java @@ -0,0 +1,38 @@ +/* Copyright (C) 2019 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.io.Serializable; +import java.util.Date; + +public interface Reminder extends Serializable { + /** + * The {@link android.os.Bundle} name for transferring parceled reminders. + */ + String EXTRA_REMINDER = "reminder"; + + int ONCE = 0; + int EVERY_DAY = 1; + int EVERY_WEEK = 2; + int EVERY_MONTH = 3; + int EVERY_YEAR = 4; + + String getReminderId(); + String getMessage(); + Date getDate(); + int getRepetition(); +} 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 c7e0f1013..148ebb465 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -73,6 +73,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.receivers.AutoConnectIntervalReceiver; import nodomain.freeyourgadget.gadgetbridge.service.receivers.GBAutoFetchReceiver; @@ -117,6 +118,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE 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_LED_COLOR; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_REMINDERS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION; @@ -170,6 +172,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TITLE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TYPE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RECORDED_DATA_TYPES; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_REMINDERS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RESET_FLAGS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_VIBRATION_INTENSITY; @@ -578,6 +581,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere ArrayList alarms = (ArrayList) intent.getSerializableExtra(EXTRA_ALARMS); mDeviceSupport.onSetAlarms(alarms); break; + case ACTION_SET_REMINDERS: + ArrayList reminders = (ArrayList) intent.getSerializableExtra(EXTRA_REMINDERS); + mDeviceSupport.onSetReminders(reminders); + break; case ACTION_ENABLE_REALTIME_STEPS: { boolean enable = intent.getBooleanExtra(EXTRA_BOOLEAN_ENABLE, false); mDeviceSupport.onEnableRealtimeSteps(enable); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index b1b5769f6..bd60f8342 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; /** @@ -309,6 +310,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onSetAlarms(alarms); } + @Override + public void onSetReminders(ArrayList reminders) { + if (checkBusy("set reminders")) { + return; + } + delegate.onSetReminders(reminders); + } + @Override public void onEnableRealtimeSteps(boolean enable) { if (checkBusy("enable realtime steps: " + enable)) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index fdf0d14ad..a51d887d9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -36,6 +36,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; @@ -366,6 +367,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im } + @Override + public void onSetReminders(ArrayList reminders) { + + } + @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 6147e9b2a..8246b6493 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -107,6 +107,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; @@ -150,6 +151,7 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_RESERVER_ALARMS_CALENDAR; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_RESERVER_REMINDERS_CALENDAR; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDS; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT; @@ -778,6 +780,117 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } } + @Override + public void onSetReminders(ArrayList reminders) { + final TransactionBuilder builder; + try { + builder = performInitialized("onSetReminders"); + } catch (final IOException e) { + LOG.error("Unable to send reminders to device", e); + return; + } + + sendReminders(builder, reminders); + + builder.queue(getQueue()); + } + + private void sendReminders(final TransactionBuilder builder) { + final List reminders = DBHelper.getReminders(gbDevice); + sendReminders(builder, reminders); + } + + private void sendReminders(final TransactionBuilder builder, final List reminders) { + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); + + final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); + int reservedSlots = prefs.getInt(PREF_RESERVER_REMINDERS_CALENDAR, 9); + LOG.info("On Set Reminders. Reminders: {}, Reserved slots: {}", reminders.size(), reservedSlots); + + // Send the reminders, skipping the reserved slots for calendar events + for (int i = 0; i < reminders.size(); i++) { + LOG.debug("Sending reminder at position {}", i + reservedSlots); + + sendReminderToDevice(builder, i + reservedSlots, reminders.get(i)); + } + + // Delete the remaining slots, skipping the sent reminders and reserved slots + for (int i = reminders.size() + reservedSlots; i < coordinator.getReminderSlotCount(); i++) { + LOG.debug("Deleting reminder at position {}", i); + + sendReminderToDevice(builder, i, null); + } + } + + private void sendReminderToDevice(final TransactionBuilder builder, int position, final Reminder reminder) { + if (characteristicChunked == null) { + LOG.warn("characteristicChunked is null, not sending reminder"); + return; + } + + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); + + if (position + 1 > coordinator.getReminderSlotCount()) { + LOG.error("Reminder for position {} is over the limit of {} reminders", position, coordinator.getReminderSlotCount()); + return; + } + + if (reminder == null) { + // Delete reminder + writeToChunked(builder, 3, new byte[]{(byte) (position & 0xFF), 0, 0, 0, 0, 0}); + + return; + } + + final ByteBuffer buf = ByteBuffer.allocate(14 + reminder.getMessage().getBytes().length); + buf.order(ByteOrder.LITTLE_ENDIAN); + + buf.put((byte) 0x0B); + buf.put((byte) (position & 0xFF)); + + switch(reminder.getRepetition()) { + case Reminder.ONCE: + buf.put(new byte[]{0x09, 0x00}); + break; + case Reminder.EVERY_DAY: + buf.put(new byte[]{(byte) 0xE9, 0x0F}); + break; + case Reminder.EVERY_WEEK: + buf.put(new byte[]{0x09, 0x01}); + break; + case Reminder.EVERY_MONTH: + buf.put(new byte[]{0x09, 0x10}); + break; + case Reminder.EVERY_YEAR: + buf.put(new byte[]{0x09, 0x20}); + break; + default: + LOG.warn("Unknown repetition for reminder in position {}, defaulting to once", position); + buf.put(new byte[]{0x09, 0x00}); + } + + buf.put(new byte[]{0x00, 0x00}); // unknown + + final Calendar cal = Calendar.getInstance(); + cal.setTime(reminder.getDate()); + + buf.put(BLETypeConversions.shortCalendarToRawBytes(cal)); + buf.put((byte) 0x00); + + if (reminder.getMessage().getBytes().length > coordinator.getMaximumReminderMessageLength()) { + LOG.warn("The reminder message length {} is longer than {}, will be truncated", + reminder.getMessage().getBytes().length, + coordinator.getMaximumReminderMessageLength() + ); + buf.put(Arrays.copyOf(reminder.getMessage().getBytes(), coordinator.getMaximumReminderMessageLength())); + } else { + buf.put(reminder.getMessage().getBytes()); + } + buf.put((byte) 0x00); + + writeToChunked(builder, 2, buf.array()); + } + @Override public void onNotification(NotificationSpec notificationSpec) { if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) { @@ -1903,19 +2016,21 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { return this; } + final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())); + int availableSlots = prefs.getInt(PREF_RESERVER_REMINDERS_CALENDAR, 9); + CalendarEvents upcomingEvents = new CalendarEvents(); List calendarEvents = upcomingEvents.getCalendarEventList(getContext()); Calendar calendar = Calendar.getInstance(); int iteration = 0; - int iterationMax = 8; for (CalendarEvents.CalendarEvent calendarEvent : calendarEvents) { if (calendarEvent.isAllDay()) { continue; } - if (iteration > iterationMax) { // limit ? + if (iteration >= availableSlots) { break; } @@ -1948,7 +2063,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } // Continue by deleting the events - for(;iteration < iterationMax; iteration++){ + for(;iteration < availableSlots; iteration++){ int length = 1 + 1 + 4 + 6 + 6 + 1 + 0 + 1; ByteBuffer buf = ByteBuffer.allocate(length); @@ -3014,6 +3129,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { setDisconnectNotification(builder); setExposeHRThridParty(builder); setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval()); + sendReminders(builder); requestAlarms(builder); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index 3c23c834f..d880f8aae 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -17,6 +17,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.serial; +import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; @@ -28,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; @@ -262,4 +264,10 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport byte[] bytes = gbDeviceProtocol.encodeLedColor(color); sendToDevice(bytes); } + + @Override + public void onSetReminders(ArrayList reminders) { + byte[] bytes = gbDeviceProtocol.encodeReminders(reminders); + sendToDevice(bytes); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index 7ca8d5cc2..ccf3adb52 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -17,6 +17,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.serial; +import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; @@ -24,6 +25,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; public abstract class GBDeviceProtocol { @@ -141,6 +143,10 @@ public abstract class GBDeviceProtocol { return null; } + public byte[] encodeReminders(ArrayList reminders) { + return null; + } + public byte[] encodeFmFrequency(float frequency) { return null; } diff --git a/app/src/main/res/drawable/ic_device_set_reminders.xml b/app/src/main/res/drawable/ic_device_set_reminders.xml new file mode 100644 index 000000000..7009a6763 --- /dev/null +++ b/app/src/main/res/drawable/ic_device_set_reminders.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 000000000..a7a81a25d --- /dev/null +++ b/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_configure_reminders.xml b/app/src/main/res/layout/activity_configure_reminders.xml new file mode 100644 index 000000000..fbbc4cf8f --- /dev/null +++ b/app/src/main/res/layout/activity_configure_reminders.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_reminder_details.xml b/app/src/main/res/layout/activity_reminder_details.xml new file mode 100644 index 000000000..294fef19e --- /dev/null +++ b/app/src/main/res/layout/activity_reminder_details.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/device_itemv2.xml b/app/src/main/res/layout/device_itemv2.xml index 7dd574733..2dcbefaa0 100644 --- a/app/src/main/res/layout/device_itemv2.xml +++ b/app/src/main/res/layout/device_itemv2.xml @@ -370,7 +370,7 @@ card_view:tint="@color/secondarytext" /> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 4b01fd8a4..53bf80961 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -222,8 +222,23 @@ Cancele para parar a vibração. Atividade e sono Configurar alarmes + Configurar Lembretes Configurar alarmes + Configurar lembretes Detalhes do alarme + Detalhes do lembrete + %1$s, uma vez + %1$s, todos os dias + %1$s, todas as semanas + %1$s, todos os meses + %1$s, todos os anos + Uma vez + Todos os dias + Todas as semanas + Todos os meses + Todos os anos + Apagar lembrete + De certeza que pretende apagar o lembrete? Dom Seg Ter diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 29d8ab387..6e696d523 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1490,6 +1490,14 @@ 1800 + + @string/reminder_once + @string/reminder_every_day + @string/reminder_every_week + @string/reminder_every_month + @string/reminder_every_year + + @string/filter_mode_none @string/filter_mode_whitelist diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8900ce1d6..bd2b0372b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -451,8 +451,29 @@ Cancel to stop vibration. Activity and Sleep Configure alarms + Configure reminders Configure alarms + Configure reminders + Repeat + Date + Time + Message + %1$s, once + %1$s, every day + %1$s, every week + %1$s, every month + %1$s, every year + Once + Every day + Every week + Every month + Every year + Delete reminder + Are you sure you want to delete the reminder? + No free slots + The device has no free slots for reminders (total slots: %1$s) Alarm details + Reminder details Sun Mon Tue @@ -577,6 +598,7 @@ Incompatible firmware This firmware is not compatible with the device Alarms to reserve for upcoming events + Reminders to reserve for upcoming events Use heart rate sensor to improve sleep detection Device time offset in hours (for detecting sleep of shift workers) Find phone diff --git a/app/src/main/res/xml/devicesettings_reserve_reminders_calendar.xml b/app/src/main/res/xml/devicesettings_reserve_reminders_calendar.xml new file mode 100644 index 000000000..e7473ed64 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_reserve_reminders_calendar.xml @@ -0,0 +1,13 @@ + + + + + +