1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-09-27 16:56:57 +02:00

Compare commits

...

4 Commits

Author SHA1 Message Date
José Rebelo
d946053bcb Garmin: Allow fetching unknown files 2024-06-16 22:21:46 +00:00
Arjan Schrijver
bad29bbeb8 Dashboard: Add monthly goals indicator and legend to calendar view 2024-06-16 23:36:19 +02:00
José Rebelo
afff822ab1 Garmin Instinct 2X Solar: Fix recognition of some watches 2024-06-16 17:38:42 +01:00
José Rebelo
5be0c612c6 Garmin Venu 2: Initial support 2024-06-16 17:35:00 +01:00
11 changed files with 234 additions and 48 deletions

View File

@ -17,14 +17,23 @@
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorInt;
@ -56,6 +65,13 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
private final ConcurrentHashMap<Calendar, TextView> dayCells = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Integer> dayColors = new ConcurrentHashMap<>();
@ColorInt private int color_unknown = Color.argb(50, 128, 128, 128);
@ColorInt private int color_0_25 = Color.argb(128, 255, 0, 0); // Red
@ColorInt private int color_25_50 = Color.argb(128, 255, 128, 0); // Orange
@ColorInt private int color_50_75 = Color.argb(128, 255, 255, 0); // Yellow
@ColorInt private int color_75_100 = Color.argb(128, 0, 128, 0); // Dark green
@ColorInt private int color_100 = Color.argb(128, 0, 255, 0); // Green
private boolean showAllDevices;
private Set<String> showDeviceList;
@ -65,6 +81,8 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
GridLayout calendarGrid;
Calendar currentDay;
Calendar cal;
ImageView monthGoalsChart;
TextView monthGoalsText;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -72,6 +90,8 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
setContentView(R.layout.activity_dashboard_calendar);
monthTextView = findViewById(R.id.calendar_month);
calendarGrid = findViewById(R.id.dashboard_calendar_grid);
monthGoalsChart = findViewById(R.id.dashboard_calendar_month_goals_chart);
monthGoalsText = findViewById(R.id.dashboard_calendar_month_goals_text);
currentDay = Calendar.getInstance();
cal = Calendar.getInstance();
long receivedTimestamp = getIntent().getLongExtra(EXTRA_TIMESTAMP, 0);
@ -142,7 +162,11 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
// Determine last day that should be displayed
Calendar lastDay = (Calendar) cal.clone();
lastDay.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
lastDay.add(Calendar.DAY_OF_MONTH, (firstDayOfWeek + 7 - lastDay.get(Calendar.DAY_OF_WEEK)) % 7);
int daysAfterMonth = (firstDayOfWeek + 7 - lastDay.get(Calendar.DAY_OF_WEEK)) % 7;
if (daysAfterMonth == 0 && lastDay.get(Calendar.DAY_OF_WEEK) == firstDayOfWeek) {
daysAfterMonth = 7;
}
lastDay.add(Calendar.DAY_OF_MONTH, daysAfterMonth);
// Add day names header
SimpleDateFormat dayFormat = new SimpleDateFormat("E", Locale.getDefault());
Calendar weekdays = Calendar.getInstance();
@ -194,6 +218,12 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
}
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
int amount_0_25 = 0;
int amount_25_50 = 0;
int amount_50_75 = 0;
int amount_75_100 = 0;
int amount_100 = 0;
@Override
protected Void doInBackground(Void... params) {
for (Calendar day : dayCells.keySet()) {
@ -206,17 +236,22 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
float goalFactor = DashboardUtils.getStepsGoalFactor(dashboardData);
@ColorInt int dayColor;
if (goalFactor >= 1) {
dayColor = Color.argb(128, 0, 255, 0); // Green
dayColor = color_100;
amount_100++;
} else if (goalFactor >= 0.75) {
dayColor = Color.argb(128, 0, 128, 0); // Dark green
dayColor = color_75_100;
amount_75_100++;
} else if (goalFactor >= 0.5) {
dayColor = Color.argb(128, 255, 255, 0); // Yellow
} else if (goalFactor > 0.25) {
dayColor = Color.argb(128, 255, 128, 0); // Orange
dayColor = color_50_75;
amount_50_75++;
} else if (goalFactor >= 0.25) {
dayColor = color_25_50;
amount_25_50++;
} else if (goalFactor > 0) {
dayColor = Color.argb(128, 255, 0, 0); // Red
dayColor = color_0_25;
amount_0_25++;
} else {
dayColor = Color.argb(50, 128, 128, 128);
dayColor = color_unknown;
}
dayColors.put(day.get(Calendar.DAY_OF_MONTH), dayColor);
}
@ -257,6 +292,83 @@ public class DashboardCalendarActivity extends AbstractGBActivity {
finish();
});
}
// Draw visual representation of this month's day goals
drawMonthGoalsLine();
// Fill legend
Resources res = getResources();
SpannableString line_100 = new SpannableString("■ 100%: " + res.getQuantityString(R.plurals.amount_of_days, amount_100, amount_100));
line_100.setSpan(new ForegroundColorSpan(color_100), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString line_75_100 = new SpannableString("■ 75-100%: " + res.getQuantityString(R.plurals.amount_of_days, amount_75_100, amount_75_100));
line_75_100.setSpan(new ForegroundColorSpan(color_75_100), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString line_50_75 = new SpannableString("■ 50-75%: " + res.getQuantityString(R.plurals.amount_of_days, amount_50_75, amount_50_75));
line_50_75.setSpan(new ForegroundColorSpan(color_50_75), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString line_25_50 = new SpannableString("■ 25-50%: " + res.getQuantityString(R.plurals.amount_of_days, amount_25_50, amount_25_50));
line_25_50.setSpan(new ForegroundColorSpan(color_25_50), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString line_0_25 = new SpannableString("■ 0-25%: " + res.getQuantityString(R.plurals.amount_of_days, amount_0_25, amount_0_25));
line_0_25.setSpan(new ForegroundColorSpan(color_0_25), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder builder = new SpannableStringBuilder();
monthGoalsText.setText(builder.append(line_100).append("\n").append(line_75_100).append("\n").append(line_50_75).append("\n").append(line_25_50).append("\n").append(line_0_25));
}
private void drawMonthGoalsLine() {
int monthMaxDays = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
int amountOfDays = amount_0_25 + amount_25_50 + amount_50_75 + amount_75_100 + amount_100;
int width = 700;
int height = 40;
int totalDrawWidth = width - height / 2;
float drawWidth = totalDrawWidth * ((float) amountOfDays / monthMaxDays);
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(height);
paint.setColor(color_unknown);
canvas.drawLine(height / 2, height / 2, totalDrawWidth, height / 2, paint);
// 0-25%
if (amount_0_25 > 0) {
paint.setColor(color_0_25);
canvas.drawLine(height / 2, height / 2, drawWidth, height / 2, paint);
}
// 25-50%
if (amount_25_50 > 0) {
paint.setColor(color_25_50);
float barDays = amount_25_50 + amount_50_75 + amount_75_100 + amount_100;
float barFraction = barDays / amountOfDays;
canvas.drawLine(height / 2, height / 2, drawWidth * barFraction, height / 2, paint);
}
// 50-75%
if (amount_50_75 > 0) {
paint.setColor(color_50_75);
float barDays = amount_50_75 + amount_75_100 + amount_100;
float barFraction = barDays / amountOfDays;
canvas.drawLine(height / 2, height / 2, drawWidth * barFraction, height / 2, paint);
}
// 75-100%
if (amount_75_100 > 0) {
paint.setColor(color_75_100);
float barDays = amount_75_100 + amount_100;
float barFraction = barDays / amountOfDays;
canvas.drawLine(height / 2, height / 2, drawWidth * barFraction, height / 2, paint);
}
// 100%
if (amount_100 > 0) {
paint.setColor(color_100);
float barDays = amount_100;
float barFraction = barDays / amountOfDays;
canvas.drawLine(height / 2, height / 2, drawWidth * barFraction, height / 2, paint);
}
monthGoalsChart.setImageBitmap(bitmap);
}
}
}

View File

@ -127,6 +127,7 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
developer.add(R.xml.devicesettings_keep_activity_data_on_device);
developer.add(R.xml.devicesettings_fetch_unknown_files);
return deviceSpecificSettings;
}

View File

@ -8,7 +8,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
public class GarminInstinct2XSolarCoordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("^Instinct 2X Sol$");
// Allow ending both with "Sol" (#3063) and "Solar" (reported on Matrix).
return Pattern.compile("^Instinct 2X Sol(ar)?$");
}
@Override

View File

@ -0,0 +1,18 @@
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.venu;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
public class GarminVenu2Coordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("^Venu 2$");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_venu_2;
}
}

View File

@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.Garm
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SolarCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SolTacCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinctCrossoverCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.venu.GarminVenu2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.venu.GarminVenu2PlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.venu.GarminVenu3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.venu.GarminVenu3SCoordinator;
@ -369,6 +370,7 @@ public enum DeviceType {
GARMIN_INSTINCT_2_SOLTAC(GarminInstinct2SolTacCoordinator.class),
GARMIN_INSTINCT_CROSSOVER(GarminInstinctCrossoverCoordinator.class),
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
GARMIN_VENU_2(GarminVenu2Coordinator.class),
GARMIN_VENU_2_PLUS(GarminVenu2PlusCoordinator.class),
GARMIN_VENU_3(GarminVenu3Coordinator.class),
GARMIN_VENU_3S(GarminVenu3SCoordinator.class),

View File

@ -776,8 +776,8 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
LocalBroadcastManager.getInstance(context).sendBroadcast(messageIntent);
}
public Prefs getDevicePrefs() {
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
public DevicePrefs getDevicePrefs() {
return GBApplication.getDevicePrefs(gbDevice.getAddress());
}
@Override

View File

@ -159,6 +159,7 @@ public class FileTransferHandler implements MessageHandler {
throw new IllegalArgumentException("Invalid directory data length");
final GarminByteBufferReader reader = new GarminByteBufferReader(currentlyDownloading.dataHolder.array());
reader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
final boolean fetchUnknownFiles = deviceSupport.getDevicePrefs().getFetchUnknownFiles();
while (reader.remaining() > 0) {
final int fileIndex = reader.readShort();//2
final int fileDataType = reader.readByte();//3
@ -175,7 +176,7 @@ public class FileTransferHandler implements MessageHandler {
LOG.warn("Unsupported directory entry of type {}/{}", fileDataType, fileSubType);
continue;
}
if (!FILE_TYPES_TO_PROCESS.contains(directoryEntry.filetype)) {
if (!FILE_TYPES_TO_PROCESS.contains(directoryEntry.filetype) && !fetchUnknownFiles) {
continue;
}
LOG.debug("Queueing {} for download", directoryEntry);
@ -367,7 +368,7 @@ public class FileTransferHandler implements MessageHandler {
public String toString() {
return "DirectoryEntry{" +
"fileIndex=" + fileIndex +
", fileType=" + filetype.name() +
", fileType=" + filetype +
", fileNumber=" + fileNumber +
", specificFlags=" + specificFlags +
", fileFlags=" + fileFlags +

View File

@ -31,4 +31,8 @@ public class DevicePrefs extends Prefs {
public int getBatteryNotifyFullThreshold(final BatteryConfig batteryConfig) {
return getInt(PREF_BATTERY_NOTIFY_FULL_THRESHOLD + batteryConfig.getBatteryIndex(), batteryConfig.getDefaultFullThreshold());
}
public boolean getFetchUnknownFiles() {
return getBoolean("fetch_unknown_files", false);
}
}

View File

@ -1,49 +1,78 @@
<android.widget.LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activities.dashboard.DashboardCalendarActivity">
<RelativeLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/arrow_left"
android:layout_width="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003C"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentLeft="true" />
android:layout_margin="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/arrow_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003C"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentLeft="true" />
<TextView
android:id="@+id/calendar_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/calendar_month"
android:textStyle="bold"
android:textSize="25sp"
android:layout_centerInParent="true" />
<Button
android:id="@+id/arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003E"
android:textStyle="bold"
android:textSize="20sp"
android:layout_alignParentRight="true" />
</RelativeLayout>
<androidx.gridlayout.widget.GridLayout
android:id="@+id/dashboard_calendar_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
app:columnCount="7" />
<TextView
android:id="@+id/calendar_month"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/calendar_month"
android:textStyle="bold"
android:textSize="25sp"
android:layout_centerInParent="true" />
<Button
android:id="@+id/arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:text="\u003E"
android:textStyle="bold"
android:layout_margin="8dp"
android:textSize="20sp"
android:layout_alignParentRight="true" />
</RelativeLayout>
android:text="@string/dashboard_calendar_month_goals_reached_title" />
<androidx.gridlayout.widget.GridLayout
android:id="@+id/dashboard_calendar_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
app:columnCount="7" />
<ImageView
android:id="@+id/dashboard_calendar_month_goals_chart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true" />
</android.widget.LinearLayout>
<TextView
android:id="@+id/dashboard_calendar_month_goals_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp" />
</LinearLayout>
</ScrollView>

View File

@ -1497,6 +1497,7 @@
<string name="devicetype_zepp_e">Zepp E</string>
<string name="devicetype_garmin_vivomove_hr">Garmin Vívomove HR</string>
<string name="devicetype_garmin_vivomove_style">Garmin Vívomove Style</string>
<string name="devicetype_garmin_venu_2">Garmin Venu 2</string>
<string name="devicetype_garmin_venu_2_plus">Garmin Venu 2 Plus</string>
<string name="devicetype_garmin_venu_3">Garmin Venu 3</string>
<string name="devicetype_garmin_venu_3s">Garmin Venu 3S</string>
@ -2839,6 +2840,11 @@
<string name="pref_dashboard_select_devices_summary">Combine activity data from specific devices for the totals on the dashboard</string>
<string name="pref_dashboard_widget_today_hr_interval_title">Heart rate interval</string>
<string name="pref_dashboard_widget_today_hr_interval_summary">The amount of minutes the chart shows \'worn\' after each successful heart rate measurement</string>
<plurals name="amount_of_days">
<item quantity="one">%d day</item>
<item quantity="other">%d days</item>
</plurals>
<string name="dashboard_calendar_month_goals_reached_title">% of steps goal reached</string>
<string name="pref_auto_reply_calls_summary">The phone will automatically pick-up incoming phonecalls</string>
<string name="pref_auto_reply_calls_title">Automatically answer phone calls</string>
<string name="pref_auto_reply_calls_delay_summary">Number of seconds after which the call is automatically picked up</string>
@ -2916,4 +2922,6 @@
<string name="battery_full_threshold">Full battery threshold</string>
<string name="default_percentage">Default (%1$d%%)</string>
<string name="battery_percentage_str">%1$s%%</string>
<string name="pref_fetch_unknown_files_title">Fetch unknown files</string>
<string name="pref_fetch_unknown_files_summary">Fetch unknown activity files from the watch. They will not be processed, but will be saved in the phone.</string>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_activity_unknown_small"
android:key="fetch_unknown_files"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_fetch_unknown_files_summary"
android:title="@string/pref_fetch_unknown_files_title" />
</androidx.preference.PreferenceScreen>