diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6e962b68c..dadfcfa39 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -445,7 +445,8 @@ android:resource="@xml/shared_paths" /> - + @@ -456,6 +457,26 @@ android:resource="@xml/sleep_alarm_widget_info" /> + + + + + + + + + + + + . */ +package nodomain.freeyourgadget.gadgetbridge; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.widget.RemoteViews; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; +import nodomain.freeyourgadget.gadgetbridge.activities.WidgetAlarmsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals; +import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + + +public class Widget extends AppWidgetProvider { + public static final String WIDGET_CLICK = "nodomain.freeyourgadget.gadgetbridge.WidgetClick"; + public static final String NEW_DATA_ACTION = "nodomain.freeyourgadget.gadgetbridge.NewDataTrigger"; + public static final String APPWIDGET_DELETED = "nodomain.freeyourgadget.gadgetbridge.APPWIDGET_DELETED"; + public static final String ACTION_DEVICE_CHANGED = "nodomain.freeyourgadget.gadgetbridge.gbdevice.action.device_changed"; + private static final Logger LOG = LoggerFactory.getLogger(Widget.class); + static BroadcastReceiver broadcastReceiver = null; + + private GBDevice getSelectedDevice() { + + Context context = GBApplication.getContext(); + + if (!(context instanceof GBApplication)) { + return null; + } + + GBApplication gbApp = (GBApplication) context; + + return gbApp.getDeviceManager().getSelectedDevice(); + } + + private float[] getSteps() { + Context context = GBApplication.getContext(); + Calendar day = GregorianCalendar.getInstance(); + + if (!(context instanceof GBApplication)) { + return new float[]{0, 0, 0}; + } + DailyTotals ds = new DailyTotals(); + return ds.getDailyTotalsForAllDevices(day); + } + + private String getHM(long value) { + return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES); + } + + private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + GBDevice device = getSelectedDevice(); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget); + + //onclick refresh + Intent intent = new Intent(context, Widget.class); + intent.setAction(WIDGET_CLICK); + PendingIntent refreshDataIntent = PendingIntent.getBroadcast( + context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + //views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, refreshDataIntent); + views.setOnClickPendingIntent(R.id.todaywidget_header_bar, refreshDataIntent); + + //open GB main window + Intent startMainIntent = new Intent(context, ControlCenterv2.class); + PendingIntent startMainPIntent = PendingIntent.getActivity(context, 0, startMainIntent, 0); + views.setOnClickPendingIntent(R.id.todaywidget_header_icon, startMainPIntent); + + //alarms popup menu + Intent startAlarmListIntent = new Intent(context, WidgetAlarmsActivity.class); + PendingIntent startAlarmListPIntent = PendingIntent.getActivity(context, 0, startAlarmListIntent, 0); + views.setOnClickPendingIntent(R.id.todaywidget_header_plus, startAlarmListPIntent); + + //charts, requires device + if (device != null) { + Intent startChartsIntent = new Intent(context, ChartsActivity.class); + startChartsIntent.putExtra(GBDevice.EXTRA_DEVICE, device); + PendingIntent startChartsPIntent = PendingIntent.getActivity(context, 0, startChartsIntent, 0); + views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, startChartsPIntent); + } + + + float[] DailyTotals = getSteps(); + + views.setTextViewText(R.id.todaywidget_steps, context.getString(R.string.widget_steps_label, (int) DailyTotals[0])); + views.setTextViewText(R.id.todaywidget_sleep, context.getString(R.string.widget_sleep_label, getHM((long) DailyTotals[1]))); + + if (device != null) { + String status = String.format("%1s", device.getStateString()); + if (device.isConnected()) { + if (device.getBatteryLevel() > 1) { + status = String.format("Battery %1s%%", device.getBatteryLevel()); + } + } + + views.setTextViewText(R.id.todaywidget_device_status, status); + views.setTextViewText(R.id.todaywidget_device_name, device.getName()); + + } + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + public void refreshData() { + Context context = GBApplication.getContext(); + GBDevice device = getSelectedDevice(); + + if (device == null || !device.isInitialized()) { + GB.toast(context, + context.getString(R.string.device_not_connected), + Toast.LENGTH_SHORT, GB.ERROR); + GBApplication.deviceService().connect(); + GB.toast(context, + context.getString(R.string.connecting), + Toast.LENGTH_SHORT, GB.INFO); + + return; + } + GB.toast(context, + context.getString(R.string.busy_task_fetch_activity_data), + Toast.LENGTH_SHORT, GB.INFO); + + GBApplication.deviceService().onFetchRecordedData(RecordedDataTypes.TYPE_ACTIVITY); + } + + public void updateWidget() { + Context context = GBApplication.getContext(); + + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + ComponentName thisAppWidget = new ComponentName(context.getPackageName(), Widget.class.getName()); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget); + + onUpdate(context, appWidgetManager, appWidgetIds); + + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // There may be multiple widgets active, so update all of them + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + + @Override + public void onEnabled(Context context) { + if (this.broadcastReceiver == null) { + LOG.debug("gbwidget BROADCAST receiver initialized."); + this.broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + LOG.debug("gbwidget BROADCAST, action" + intent.getAction()); + updateWidget(); + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(NEW_DATA_ACTION); + intentFilter.addAction(ACTION_DEVICE_CHANGED); + LocalBroadcastManager.getInstance(context).registerReceiver(this.broadcastReceiver, intentFilter); + } + } + + @Override + public void onDisabled(Context context) { + + if (this.broadcastReceiver != null) { + LocalBroadcastManager.getInstance(context).unregisterReceiver(this.broadcastReceiver); + this.broadcastReceiver=null; + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + LOG.debug("gbwidget LOCAL onReceive, action: " + intent.getAction()); + //this handles widget re-connection after apk updates + if (WIDGET_CLICK.equals(intent.getAction())) { + if (this.broadcastReceiver == null) { + onEnabled(context); + } + refreshData(); + //updateWidget(); + } else if (APPWIDGET_DELETED.equals(intent.getAction())) { + onDisabled(context); + } + } + +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java new file mode 100644 index 000000000..a58924d2a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java @@ -0,0 +1,127 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class WidgetAlarmsActivity extends Activity implements View.OnClickListener { + + TextView textView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context appContext = this.getApplicationContext(); + if (appContext instanceof GBApplication) { + GBApplication gbApp = (GBApplication) appContext; + GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice(); + if (selectedDevice == null || !selectedDevice.isInitialized()) { + GB.toast(this, + this.getString(R.string.not_connected), + Toast.LENGTH_LONG, GB.WARN); + + } else { + setContentView(R.layout.widget_alarms_activity_list); + int userSleepDuration = new ActivityUser().getSleepDuration(); + textView = findViewById(R.id.alarm5); + if (userSleepDuration > 0) { + textView.setText(String.format(this.getString(R.string.widget_alarm_target_hours), userSleepDuration)); + } else { + textView.setVisibility(View.GONE); + } + } + } + } + + @Override + public void onClick(View v) { + + switch (v.getId()) { + case R.id.alarm1: + setAlarm(5); + break; + case R.id.alarm2: + setAlarm(10); + break; + case R.id.alarm3: + setAlarm(20); + break; + case R.id.alarm4: + setAlarm(60); + break; + case R.id.alarm5: + setAlarm(0); + break; + default: + break; + } + //this is to prevent screen flashing during closing + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 150); + } + + public void setAlarm(int duration) { + // current timestamp + GregorianCalendar calendar = new GregorianCalendar(); + if (duration > 0) { + calendar.add(Calendar.MINUTE, duration); + } else { + int userSleepDuration = new ActivityUser().getSleepDuration(); + // add preferred sleep duration + if (userSleepDuration > 0) { + calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration); + } else { // probably testing + calendar.add(Calendar.MINUTE, 1); + } + } + + // overwrite the first alarm and activate it, without + + Context appContext = this.getApplicationContext(); + if (appContext instanceof GBApplication) { + GBApplication gbApp = (GBApplication) appContext; + GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice(); + if (selectedDevice == null || !selectedDevice.isInitialized()) { + GB.toast(this, + this.getString(R.string.appwidget_not_connected), + Toast.LENGTH_LONG, GB.WARN); + return; + } + } + + int hours = calendar.get(Calendar.HOUR_OF_DAY); + int minutes = calendar.get(Calendar.MINUTE); + + GB.toast(this, + this.getString(R.string.appwidget_setting_alarm, hours, minutes), + Toast.LENGTH_SHORT, GB.INFO); + + Alarm alarm = AlarmUtils.createSingleShot(0, true, calendar); + ArrayList alarms = new ArrayList<>(1); + alarms.add(alarm); + GBApplication.deviceService().onSetAlarms(alarms); + + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java index 155dcf7b2..25e7d342e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java @@ -28,15 +28,15 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -class ActivityAnalysis { - private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class); +public class ActivityAnalysis { + public static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class); // store raw steps and duration protected HashMap stats = new HashMap(); // max speed determined from samples private int maxSpeed = 0; - ActivityAmounts calculateActivityAmounts(List samples) { + public ActivityAmounts calculateActivityAmounts(List samples) { ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP); ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP); ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java new file mode 100644 index 000000000..bcd8ff73e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -0,0 +1,155 @@ +/* Copyright (C) 2017-2019 Carsten Pfeiffer, Daniele Gobbetti + + 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 android.content.Context; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityAnalysis; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + + + +public class DailyTotals { + Logger LOG = LoggerFactory.getLogger(DailyTotals.class); + + + public float[] getDailyTotalsForAllDevices(Calendar day) { + Context context = GBApplication.getContext(); + //get today's steps for all devices in GB + float all_steps = 0; + float all_sleep = 0; + + + if (context instanceof GBApplication) { + GBApplication gbApp = (GBApplication) context; + List devices = gbApp.getDeviceManager().getDevices(); + for (GBDevice device : devices) { + float[] all_daily = getDailyTotalsForDevice(device, day); + all_steps += all_daily[0]; + all_sleep += all_daily[1] + all_daily[2]; + } + } + LOG.debug("gbwidget daily totals, all steps:" + all_steps); + LOG.debug("gbwidget daily totals, all sleep:" + all_sleep); + return new float[]{all_steps, all_sleep}; + } + + + public float[] getDailyTotalsForDevice(GBDevice device, Calendar day + ) { + + try (DBHandler handler = GBApplication.acquireDB()) { + ActivityAnalysis analysis = new ActivityAnalysis(); + ActivityAmounts amountsSteps = null; + ActivityAmounts amountsSleep = null; + + amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device)); + amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device)); + + float[] Sleep = getTotalsSleepForActivityAmounts(amountsSleep); + float Steps = getTotalsStepsForActivityAmounts(amountsSteps); + + return new float[]{Steps, Sleep[0], Sleep[1]}; + + } catch (Exception e) { + + GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e); + return new float[]{0, 0, 0}; + } + } + + private float[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) { + long totalSecondsDeepSleep = 0; + long totalSecondsLightSleep = 0; + for (ActivityAmount amount : activityAmounts.getAmounts()) { + if (amount.getActivityKind() == ActivityKind.TYPE_DEEP_SLEEP) { + totalSecondsDeepSleep += amount.getTotalSeconds(); + } else if (amount.getActivityKind() == ActivityKind.TYPE_LIGHT_SLEEP) { + totalSecondsLightSleep += amount.getTotalSeconds(); + } + } + int totalMinutesDeepSleep = (int) (totalSecondsDeepSleep / 60); + int totalMinutesLightSleep = (int) (totalSecondsLightSleep / 60); + return new float[]{totalMinutesDeepSleep, totalMinutesLightSleep}; + } + + + private float getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) { + long totalSteps = 0; + float totalValue = 0; + + for (ActivityAmount amount : activityAmounts.getAmounts()) { + totalSteps += amount.getTotalSteps(); + } + + float[] totalValues = new float[]{totalSteps}; + + for (int i = 0; i < totalValues.length; i++) { + float value = totalValues[i]; + totalValue += value; + } + return totalValue; + } + + + private List getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) { + int startTs; + int endTs; + + day = (Calendar) day.clone(); // do not modify the caller's argument + day.set(Calendar.HOUR_OF_DAY, 0); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + day.add(Calendar.HOUR, offsetHours); + + startTs = (int) (day.getTimeInMillis() / 1000); + endTs = startTs + 24 * 60 * 60 - 1; + + return getSamples(db, device, startTs, endTs); + } + + + protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + return getAllSamples(db, device, tsFrom, tsTo); + } + + + protected SampleProvider getProvider(DBHandler db, GBDevice device) { + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); + return coordinator.getSampleProvider(device, db.getDaoSession()); + } + + + protected List getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider provider = getProvider(db, device); + return provider.getAllActivitySamples(tsFrom, tsTo); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java index 915db7cd7..1c9d7691b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java @@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; import android.content.SharedPreferences; import android.widget.Toast; @@ -126,6 +127,8 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation { GB.updateTransferNotification(null, "", false, 100, getContext()); operationFinished(); unsetBusy(); + Intent intent = new Intent("nodomain.freeyourgadget.gadgetbridge.NewDataTrigger"); + getContext().sendBroadcast(intent); } /** diff --git a/app/src/main/res/drawable/widget_preview.png b/app/src/main/res/drawable/widget_preview.png new file mode 100644 index 000000000..7ad186041 Binary files /dev/null and b/app/src/main/res/drawable/widget_preview.png differ diff --git a/app/src/main/res/layout/widget.xml b/app/src/main/res/layout/widget.xml new file mode 100644 index 000000000..e6aca5181 --- /dev/null +++ b/app/src/main/res/layout/widget.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_alarms_activity_list.xml b/app/src/main/res/layout/widget_alarms_activity_list.xml new file mode 100644 index 000000000..c646f189a --- /dev/null +++ b/app/src/main/res/layout/widget_alarms_activity_list.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96cc3b117..47fd8a23b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -527,10 +527,7 @@ Authenticating Authentication required - Zzz - Add widget Preferred sleep duration in hours - Setting alarm for %1$02d:%2$02d Hardware revision: %1$s Firmware version: %1$s Error creating directory for log files: %1$s @@ -715,5 +712,23 @@ Filter Mode Mode Configuration Save Configuration + Not connected, alarm not set. + Zzz + Add widget + Setting alarm for %1$02d:%2$02d + Sleep Alarm + + Steps: %1$02d + Sleep: %1$s + Status and Alarms + Set alarm after: + 5 minutes + 10 minutes + 20 minutes + 1 hour + Icon + %d hours + + diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml new file mode 100644 index 000000000..9bc0b9b98 --- /dev/null +++ b/app/src/main/res/xml/widget_info.xml @@ -0,0 +1,14 @@ + + + +