mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 12:56:49 +01:00
Merge branch 'master' into background-javascript
This commit is contained in:
commit
f4e11c8cb3
@ -1,6 +1,10 @@
|
||||
### Changelog
|
||||
|
||||
#### Version 0.20.0 (next)
|
||||
#### Version 0.20.1
|
||||
* Amazfit Bip: Support icons and text body for notifications
|
||||
* Mi Band: Fix setting smart alarms
|
||||
|
||||
#### Version 0.20.0
|
||||
* Inital Amazfit Bip support (WIP)
|
||||
* Various theming fixes
|
||||
* Add workaround for blacklist not properly persisting
|
||||
@ -8,7 +12,7 @@
|
||||
* Pebble: Pass booleans from Javascript Appmessage correctly
|
||||
* Pebble: Make local configuration pages work on most recent webview implementation
|
||||
* Pebble: Allow to blacklist calendars
|
||||
* Add greek transliteration support
|
||||
* Add Greek and German transliteration support
|
||||
* Various visual improvements to charts
|
||||
|
||||
#### Version 0.19.4
|
||||
|
@ -22,3 +22,6 @@ Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
|
||||
|
||||
Creative Commons Attribution 3.0 Unported license (CC BY-3.0):
|
||||
ic_notification_battery_low.png by Picol.org. Source: https://commons.wikimedia.org/wiki/File:Battery_1_Picol_icon.svg
|
||||
|
||||
Creative Commons Attribution 3.0 United States (CC BY-3.0 US):
|
||||
ic_donate by Peter van Driel https://thenounproject.com/term/donate/239009/
|
||||
|
@ -2,7 +2,7 @@ Gadgetbridge
|
||||
============
|
||||
|
||||
Gadgetbridge is an Android (4.4+) application which will allow you to use your
|
||||
Pebble or Mi Band or HPlus device without the vendor's closed source application
|
||||
Pebble, Mi Band, Amazfit Bit and HPlus device (and more) without the vendor's closed source application
|
||||
and without the need to create an account and transmit any of your data to the
|
||||
vendor's servers.
|
||||
|
||||
@ -23,6 +23,7 @@ vendor's servers.
|
||||
* Pebble 2 (add the device from within Gadgetbridge!) [Wiki section about pebble](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble), most parts apply to Pebble 2 as well
|
||||
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
|
||||
* Mi Band 2 [Wiki section about mi band](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), some parts apply to mi band 2 as well
|
||||
* Amazfit Bip (WIP) [Wiki section about the Amazfit Bip](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
|
||||
* HPlus Devices (e.g. ZeBand) [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
|
||||
* Liveview
|
||||
* Vibratissimo (experimental)
|
||||
|
@ -26,8 +26,8 @@ android {
|
||||
targetSdkVersion 25
|
||||
|
||||
// note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.20.0"
|
||||
versionCode 98
|
||||
versionName "0.20.1"
|
||||
versionCode 99
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -25,6 +25,7 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Canvas;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
@ -61,8 +62,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
import static de.cketti.library.changelog.ChangeLog.DEFAULT_CSS;
|
||||
|
||||
//TODO: extend GBActivity, but it requires actionbar that is not available
|
||||
public class ControlCenterv2 extends AppCompatActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
@ -250,6 +249,11 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
case R.id.action_quit:
|
||||
GBApplication.quit();
|
||||
return true;
|
||||
case R.id.donation_link:
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
return true;
|
||||
case R.id.external_changelog:
|
||||
ChangeLog cl = createChangeLog();
|
||||
cl.getFullLogDialog().show();
|
||||
|
@ -189,6 +189,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
|
||||
public void onClick(View v) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(context, ConfigureAlarms.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,97 @@
|
||||
/* Copyright (C) 2017 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
|
||||
public class AmazfitBipIcon {
|
||||
// icons which are unsure which app they are for are suffixed with _NN
|
||||
public static final int CHAT = 0;
|
||||
public static final int PENGUIN_1 = 1;
|
||||
public static final int MI_CHAT_2 = 2;
|
||||
public static final int FACEBOOK = 3;
|
||||
public static final int TWITTER = 4;
|
||||
public static final int MI_APP_5 = 5;
|
||||
public static final int SNAPCHAT = 6;
|
||||
public static final int WHATSAPP = 7;
|
||||
public static final int RED_WHITE_FIRE_8 = 8;
|
||||
public static final int CHINESE_9 = 9;
|
||||
public static final int ALARM_CLOCK = 10;
|
||||
public static final int APP_11 = 11;
|
||||
public static final int CAMERA_12 = 12;
|
||||
public static final int CHAT_BLUE_13 = 13;
|
||||
public static final int COW_14 = 14;
|
||||
public static final int CHINESE_15 = 15;
|
||||
public static final int CHINESE_16 = 16;
|
||||
public static final int STAR_17 = 17;
|
||||
public static final int APP_18 = 18;
|
||||
public static final int CHINESE_19 = 19;
|
||||
public static final int CHINESE_20 = 20;
|
||||
public static final int CALENDAR = 21;
|
||||
public static final int FACEBOOK_MESSENGER = 22;
|
||||
public static final int WHATSAPP_CALL_23 = 23;
|
||||
public static final int LINE = 24;
|
||||
public static final int TELEGRAM = 25;
|
||||
public static final int KAKAOTALK = 26;
|
||||
public static final int SKYPE = 27;
|
||||
public static final int VKONTAKTE = 28;
|
||||
public static final int POKEMONGO = 29;
|
||||
public static final int HANGOUTS = 30;
|
||||
public static final int MI_31 = 31;
|
||||
public static final int CHINESE_32 = 32;
|
||||
public static final int CHINESE_33 = 33;
|
||||
public static final int EMAIL = 34;
|
||||
public static final int WEATHER = 35;
|
||||
public static final int HR_WARNING_36 = 36;
|
||||
|
||||
|
||||
public static int mapToIconId(NotificationType type) {
|
||||
switch (type) {
|
||||
case UNKNOWN:
|
||||
return APP_11;
|
||||
case CONVERSATIONS:
|
||||
return CHAT;
|
||||
case GENERIC_EMAIL:
|
||||
return EMAIL;
|
||||
case GENERIC_NAVIGATION:
|
||||
return APP_11;
|
||||
case GENERIC_SMS:
|
||||
return CHAT;
|
||||
case GENERIC_CALENDAR:
|
||||
return CALENDAR;
|
||||
case FACEBOOK:
|
||||
return FACEBOOK;
|
||||
case FACEBOOK_MESSENGER:
|
||||
return FACEBOOK_MESSENGER;
|
||||
case RIOT:
|
||||
return CHAT;
|
||||
case SIGNAL:
|
||||
return CHAT_BLUE_13;
|
||||
case TWITTER:
|
||||
return TWITTER;
|
||||
case TELEGRAM:
|
||||
return TELEGRAM;
|
||||
case WHATSAPP:
|
||||
return WHATSAPP;
|
||||
case GENERIC_ALARM_CLOCK:
|
||||
return ALARM_CLOCK;
|
||||
}
|
||||
return APP_11;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* Copyright (C) 2017 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class AmazfitBipService {
|
||||
public static final UUID UUID_CHARACTERISTIC_WEATHER = UUID.fromString("0000000e-0000-3512-2118-0009af100700");
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/* Copyright (C) 2017 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
|
||||
|
||||
|
||||
public class AmazfitBipWeatherConditions {
|
||||
public static final byte CLEAR_SKY = 0;
|
||||
public static final byte SCATTERED_CLOUDS = 1;
|
||||
public static final byte CLOUDY = 2;
|
||||
public static final byte RAIN_WITH_SUN = 3;
|
||||
public static final byte THUNDERSTORM = 4;
|
||||
public static final byte HAIL = 5;
|
||||
public static final byte RAIN_AND_SNOW = 6;
|
||||
public static final byte LIGHT_RAIN = 7;
|
||||
public static final byte MEDIUM_RAIN = 8;
|
||||
public static final byte HEAVY_RAIN = 9;
|
||||
public static final byte EXTREME_RAIN = 10;
|
||||
public static final byte SUPER_EXTREME_RAIN = 11;
|
||||
public static final byte TORRENTIAL_RAIN = 12;
|
||||
public static final byte SNOW_AND_SUN = 13;
|
||||
public static final byte LIGHT_SNOW = 14;
|
||||
public static final byte MEDIUM_SNOW = 15;
|
||||
public static final byte HEAVY_SNOW = 16;
|
||||
public static final byte EXTREME_SNOW = 17;
|
||||
public static final byte MIST = 18;
|
||||
public static final byte DRIZZLE = 19;
|
||||
public static final byte WIND_AND_RAIN = 20;
|
||||
// 21- various types of rain
|
||||
|
||||
public static byte mapToAmazfitBipWeatherCode(int openWeatherMapCondition) {
|
||||
// openweathermap.org conditions:
|
||||
// http://openweathermap.org/weather-conditions
|
||||
switch (openWeatherMapCondition) {
|
||||
//Group 2xx: Thunderstorm
|
||||
case 200: //thunderstorm with light rain: //11d
|
||||
case 201: //thunderstorm with rain: //11d
|
||||
case 202: //thunderstorm with heavy rain: //11d
|
||||
case 210: //light thunderstorm:: //11d
|
||||
case 211: //thunderstorm: //11d
|
||||
case 230: //thunderstorm with light drizzle: //11d
|
||||
case 231: //thunderstorm with drizzle: //11d
|
||||
case 232: //thunderstorm with heavy drizzle: //11d
|
||||
case 212: //heavy thunderstorm: //11d
|
||||
case 221: //ragged thunderstorm: //11d
|
||||
return THUNDERSTORM;
|
||||
//Group 3xx: Drizzle
|
||||
case 300: //light intensity drizzle: //09d
|
||||
case 301: //drizzle: //09d
|
||||
case 302: //heavy intensity drizzle: //09d
|
||||
case 310: //light intensity drizzle rain: //09d
|
||||
case 311: //drizzle rain: //09d
|
||||
case 312: //heavy intensity drizzle rain: //09d
|
||||
case 313: //shower rain and drizzle: //09d
|
||||
case 314: //heavy shower rain and drizzle: //09d
|
||||
case 321: //shower drizzle: //09d
|
||||
return DRIZZLE;
|
||||
//Group 5xx: Rain
|
||||
case 500: //light rain: //10d
|
||||
return LIGHT_RAIN;
|
||||
case 501: //moderate rain: //10d
|
||||
return MEDIUM_RAIN;
|
||||
case 502: //heavy intensity rain: //10d
|
||||
return HEAVY_RAIN;
|
||||
case 503: //very heavy rain: //10d
|
||||
return EXTREME_RAIN;
|
||||
case 504: //extreme rain: //10d
|
||||
return TORRENTIAL_RAIN;
|
||||
case 511: //freezing rain: //13d
|
||||
return MEDIUM_RAIN;
|
||||
case 520: //light intensity shower rain: //09d
|
||||
return LIGHT_RAIN;
|
||||
case 521: //shower rain: //09d
|
||||
return MEDIUM_RAIN;
|
||||
case 522: //heavy intensity shower rain: //09d
|
||||
return HEAVY_RAIN;
|
||||
case 531: //ragged shower rain: //09d
|
||||
return MEDIUM_RAIN;
|
||||
//Group 6xx: Snow
|
||||
case 600: //light snow: //[[file:13d.png]]
|
||||
return LIGHT_SNOW;
|
||||
case 601: //snow: //[[file:13d.png]]
|
||||
return MEDIUM_SNOW;
|
||||
case 602: //heavy snow: //[[file:13d.png]]
|
||||
return HEAVY_SNOW;
|
||||
case 611: //sleet: //[[file:13d.png]]
|
||||
case 612: //shower sleet: //[[file:13d.png]]
|
||||
case 615: //light rain and snow: //[[file:13d.png]]
|
||||
case 616: //rain and snow: //[[file:13d.png]]
|
||||
case 620: //light shower snow: //[[file:13d.png]]
|
||||
case 621: //shower snow: //[[file:13d.png]]
|
||||
case 622: //heavy shower snow: //[[file:13d.png]]
|
||||
return RAIN_AND_SNOW;
|
||||
|
||||
//Group 7xx: Atmosphere
|
||||
case 701: //mist: //[[file:50d.png]]
|
||||
case 711: //smoke: //[[file:50d.png]]
|
||||
case 721: //haze: //[[file:50d.png]]
|
||||
case 731: //sandcase dust whirls: //[[file:50d.png]]
|
||||
case 741: //fog: //[[file:50d.png]]
|
||||
case 751: //sand: //[[file:50d.png]]
|
||||
case 761: //dust: //[[file:50d.png]]
|
||||
case 762: //volcanic ash: //[[file:50d.png]]
|
||||
case 771: //squalls: //[[file:50d.png]]
|
||||
return MIST;
|
||||
case 781: //tornado: //[[file:50d.png]]
|
||||
case 900: //tornado
|
||||
return WIND_AND_RAIN;
|
||||
//Group 800: Clear
|
||||
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
|
||||
return CLEAR_SKY;
|
||||
//Group 80x: Clouds
|
||||
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
|
||||
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
|
||||
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
|
||||
return SCATTERED_CLOUDS;
|
||||
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
|
||||
return CLOUDY;
|
||||
//Group 90x: Extreme
|
||||
case 901: //tropical storm
|
||||
return WIND_AND_RAIN;
|
||||
case 903: //cold
|
||||
case 904: //hot
|
||||
case 905: //windy
|
||||
return 0;
|
||||
case 906: //hail
|
||||
return HAIL;
|
||||
//Group 9xx: Additional
|
||||
case 951: //calm
|
||||
case 952: //light breeze
|
||||
case 953: //gentle breeze
|
||||
case 954: //moderate breeze
|
||||
case 955: //fresh breeze
|
||||
case 956: //strong breeze
|
||||
case 957: //high windcase near gale
|
||||
case 958: //gale
|
||||
case 959: //severe gale
|
||||
case 960: //storm
|
||||
case 961: //violent storm
|
||||
case 902: //hurricane
|
||||
case 962: //hurricane
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static String mapToOpenWeatherMapIcon(int openWeatherMapCondition) {
|
||||
//see https://openweathermap.org/weather-conditions
|
||||
String condition = "02d"; //generic "variable" icon
|
||||
|
||||
if (openWeatherMapCondition >= 200 && openWeatherMapCondition < 300) {
|
||||
condition = "11d";
|
||||
} else if (openWeatherMapCondition >= 300 && openWeatherMapCondition < 500) {
|
||||
condition = "09d";
|
||||
} else if (openWeatherMapCondition >= 500 && openWeatherMapCondition < 510) {
|
||||
condition = "10d";
|
||||
} else if (openWeatherMapCondition >= 511 && openWeatherMapCondition < 600) {
|
||||
condition = "09d";
|
||||
} else if (openWeatherMapCondition >= 600 && openWeatherMapCondition < 700) {
|
||||
condition = "13d";
|
||||
} else if (openWeatherMapCondition >= 700 && openWeatherMapCondition < 800) {
|
||||
condition = "50d";
|
||||
} else if (openWeatherMapCondition == 800) {
|
||||
condition = "01d"; //TODO: night?
|
||||
} else if (openWeatherMapCondition == 801) {
|
||||
condition = "02d"; //TODO: night?
|
||||
} else if (openWeatherMapCondition == 802) {
|
||||
condition = "03d"; //TODO: night?
|
||||
} else if (openWeatherMapCondition == 803 || openWeatherMapCondition == 804) {
|
||||
condition = "04d"; //TODO: night?
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
}
|
@ -23,6 +23,8 @@ import android.content.Intent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
@ -51,7 +53,7 @@ public class WeatherNotificationReceiver extends BroadcastReceiver {
|
||||
LOG.info("weather in " + weather.location + " is " + weather.currentCondition + " (" + (weather.currentTemp - 273) + "°C)");
|
||||
|
||||
WeatherSpec weatherSpec = new WeatherSpec();
|
||||
weatherSpec.timestamp = (int) (weather.queryTime / 1000);
|
||||
weatherSpec.timestamp = (int) ((weather.queryTime - SimpleTimeZone.getDefault().getOffset(weather.queryTime)) / 1000);
|
||||
weatherSpec.location = weather.location;
|
||||
weatherSpec.currentTemp = weather.currentTemp;
|
||||
weatherSpec.currentCondition = weather.currentCondition;
|
||||
|
@ -37,7 +37,8 @@ public enum AlertCategory {
|
||||
// 10-250 reserved for future use
|
||||
// 251-255 defined by service specification
|
||||
Any(255),
|
||||
Custom(-1);
|
||||
Custom(-1),
|
||||
CustomAmazfitBip(-6);
|
||||
|
||||
private final int id;
|
||||
|
||||
|
@ -33,12 +33,16 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> extends AbstractBleProfile<T> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AlertNotificationProfile.class);
|
||||
private static final int MAX_MSG_LENGTH = 18;
|
||||
private int maxLength = 18; // Mi2-ism?
|
||||
|
||||
public AlertNotificationProfile(T support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
public void setMaxLength(int maxLength) {
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public void configure(TransactionBuilder builder, AlertNotificationControl control) {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_NOTIFICATION_CONTROL_POINT);
|
||||
if (characteristic != null) {
|
||||
@ -57,21 +61,21 @@ public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> exten
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_NEW_ALERT);
|
||||
if (characteristic != null) {
|
||||
String message = StringUtils.ensureNotNull(alert.getMessage());
|
||||
if (message.length() > MAX_MSG_LENGTH && strategy == OverflowStrategy.TRUNCATE) {
|
||||
message = StringUtils.truncate(message, MAX_MSG_LENGTH);
|
||||
if (message.length() > maxLength && strategy == OverflowStrategy.TRUNCATE) {
|
||||
message = StringUtils.truncate(message, maxLength);
|
||||
}
|
||||
|
||||
int numChunks = message.length() / MAX_MSG_LENGTH;
|
||||
if (message.length() % MAX_MSG_LENGTH > 0) {
|
||||
int numChunks = message.length() / maxLength;
|
||||
if (message.length() % maxLength > 0) {
|
||||
numChunks++;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean hasAlerted = false;
|
||||
for (int i = 0; i < numChunks; i++) {
|
||||
int offset = i * MAX_MSG_LENGTH;
|
||||
int offset = i * maxLength;
|
||||
int restLength = message.length() - offset;
|
||||
message = message.substring(offset, offset + Math.min(MAX_MSG_LENGTH, restLength));
|
||||
message = message.substring(offset, offset + Math.min(maxLength, restLength));
|
||||
if (hasAlerted && message.length() == 0) {
|
||||
// no need to do it again when there is no text content
|
||||
break;
|
||||
@ -91,10 +95,17 @@ public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> exten
|
||||
}
|
||||
}
|
||||
|
||||
public void newAlert(TransactionBuilder builder, NewAlert alert) {
|
||||
newAlert(builder, alert, OverflowStrategy.TRUNCATE);
|
||||
}
|
||||
|
||||
protected byte[] getAlertMessage(NewAlert alert, String message, int chunk) throws IOException {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream(100);
|
||||
stream.write(BLETypeConversions.fromUint8(alert.getCategory().getId()));
|
||||
stream.write(BLETypeConversions.fromUint8(alert.getNumAlerts()));
|
||||
if (alert.getCategory() == AlertCategory.CustomAmazfitBip) {
|
||||
stream.write(BLETypeConversions.fromUint8(alert.getCustomIcon()));
|
||||
}
|
||||
|
||||
if (message.length() > 0) {
|
||||
stream.write(BLETypeConversions.toUtf8s(message));
|
||||
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
|
||||
|
||||
import android.icu.util.IslamicCalendar;
|
||||
|
||||
/**
|
||||
* https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.new_alert.xml&u=org.bluetooth.characteristic.new_alert.xml
|
||||
*
|
||||
@ -47,6 +49,7 @@ public class NewAlert {
|
||||
private final AlertCategory category;
|
||||
private final int numAlerts;
|
||||
private final String message;
|
||||
private int customIcon = -1;
|
||||
|
||||
public NewAlert(AlertCategory category, int /*uint8*/ numAlerts, String /*utf8s*/ message) {
|
||||
this.category = category;
|
||||
@ -54,6 +57,13 @@ public class NewAlert {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public NewAlert(AlertCategory category, int /*uint8*/ numAlerts, String /*utf8s*/ message, int customIcon) {
|
||||
this.category = category;
|
||||
this.numAlerts = numAlerts;
|
||||
this.message = message;
|
||||
this.customIcon = customIcon;
|
||||
}
|
||||
|
||||
public AlertCategory getCategory() {
|
||||
return category;
|
||||
}
|
||||
@ -65,4 +75,8 @@ public class NewAlert {
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public int getCustomIcon() {
|
||||
return customIcon;
|
||||
}
|
||||
}
|
||||
|
@ -16,17 +16,77 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipIcon;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipWeatherConditions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class AmazfitBipSupport extends MiBand2Support {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipSupport.class);
|
||||
|
||||
@Override
|
||||
public NotificationStrategy getNotificationStrategy() {
|
||||
return new AmazfitBipTextNotificationStrategy(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) {
|
||||
onAlarmClock(notificationSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
String senderOrTiltle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
|
||||
|
||||
String message = StringUtils.truncate(senderOrTiltle, 32) + "\0";
|
||||
if (notificationSpec.subject != null) {
|
||||
message += StringUtils.truncate(notificationSpec.subject, 128) + "\n\n";
|
||||
}
|
||||
if (notificationSpec.body != null) {
|
||||
message += StringUtils.truncate(notificationSpec.body, 128);
|
||||
}
|
||||
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("new notification");
|
||||
AlertNotificationProfile<?> profile = new AlertNotificationProfile(this);
|
||||
profile.setMaxLength(255); // TODO: find out real limit, certainly it is more than 18 which is default
|
||||
|
||||
int customIconId = AmazfitBipIcon.mapToIconId(notificationSpec.type);
|
||||
|
||||
AlertCategory alertCategory = AlertCategory.CustomAmazfitBip;
|
||||
|
||||
// The SMS icon for AlertCategory.SMS is unique and not available as iconId
|
||||
if (notificationSpec.type == NotificationType.GENERIC_SMS) {
|
||||
alertCategory = AlertCategory.SMS;
|
||||
}
|
||||
|
||||
NewAlert alert = new NewAlert(alertCategory, 1, message, customIconId);
|
||||
profile.newAlert(builder, alert);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to send notification to Amazfit Bip", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
CallSpec callSpec = new CallSpec();
|
||||
@ -51,4 +111,36 @@ public class AmazfitBipSupport extends MiBand2Support {
|
||||
}
|
||||
evaluateGBDeviceEvent(callCmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("Sending weather forecast");
|
||||
final byte NR_DAYS = 2;
|
||||
ByteBuffer buf = ByteBuffer.allocate(7 + 4 * NR_DAYS);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put((byte) 1);
|
||||
buf.putInt(weatherSpec.timestamp);
|
||||
buf.put((byte) 0);
|
||||
|
||||
buf.put(NR_DAYS);
|
||||
|
||||
byte condition = AmazfitBipWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
|
||||
buf.put(condition);
|
||||
buf.put(condition);
|
||||
buf.put((byte) (weatherSpec.todayMaxTemp - 273));
|
||||
buf.put((byte) (weatherSpec.todayMinTemp - 273));
|
||||
|
||||
condition = AmazfitBipWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.tomorrowConditionCode);
|
||||
|
||||
buf.put(condition);
|
||||
buf.put(condition);
|
||||
buf.put((byte) (weatherSpec.tomorrowMaxTemp - 273));
|
||||
buf.put((byte) (weatherSpec.tomorrowMinTemp - 273));
|
||||
|
||||
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
@ -16,15 +16,22 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.OverflowStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2TextNotificationStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
|
||||
// This class in no longer in use except for incoming calls
|
||||
class AmazfitBipTextNotificationStrategy extends Mi2TextNotificationStrategy {
|
||||
|
||||
AmazfitBipTextNotificationStrategy(MiBand2Support support) {
|
||||
@ -33,14 +40,28 @@ class AmazfitBipTextNotificationStrategy extends Mi2TextNotificationStrategy {
|
||||
|
||||
@Override
|
||||
protected void sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, BtLEAction extraAction, TransactionBuilder builder) {
|
||||
if (simpleNotification != null && simpleNotification.getAlertCategory() == AlertCategory.IncomingCall) {
|
||||
// incoming calls are notified solely via NewAlert including caller ID
|
||||
sendAlert(simpleNotification, builder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (simpleNotification != null && !StringUtils.isEmpty(simpleNotification.getMessage())) {
|
||||
sendAlert(simpleNotification, builder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendAlert(@NonNull SimpleNotification simpleNotification, TransactionBuilder builder) {
|
||||
AlertNotificationProfile<?> profile = new AlertNotificationProfile<>(getSupport());
|
||||
profile.setMaxLength(255); // TODO: find out real limit, certainly it is more than 18 which is default
|
||||
|
||||
AlertCategory category = simpleNotification.getAlertCategory();
|
||||
switch (simpleNotification.getAlertCategory()) {
|
||||
// only these are confirmed working so far on Amazfit Bip
|
||||
case Email:
|
||||
case IncomingCall:
|
||||
case SMS:
|
||||
break;
|
||||
// default to SMS for non working categories
|
||||
default:
|
||||
category = AlertCategory.SMS;
|
||||
}
|
||||
NewAlert alert = new NewAlert(category, 1, simpleNotification.getMessage());
|
||||
profile.newAlert(builder, alert, OverflowStrategy.TRUNCATE);
|
||||
}
|
||||
}
|
||||
|
@ -435,7 +435,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
|
||||
protected void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized(task);
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
@ -529,7 +529,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
performPreferredNotification(origin + " received", origin, simpleNotification, alertLevel, null);
|
||||
}
|
||||
|
||||
private void onAlarmClock(NotificationSpec notificationSpec) {
|
||||
protected void onAlarmClock(NotificationSpec notificationSpec) {
|
||||
alarmClockRinging = true;
|
||||
AbortTransactionAction abortAction = new StopNotificationAction(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) {
|
||||
@Override
|
||||
|
43
app/src/main/res/drawable/ic_donate.xml
Normal file
43
app/src/main/res/drawable/ic_donate.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<group android:scaleY="-1">
|
||||
<group android:translateY="-48">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:strokeWidth="0.75"
|
||||
android:strokeMiterLimit="79.8403193612775"
|
||||
android:strokeLineCap="round"
|
||||
android:pathData="M 17.266,14.156 C 17.266,16.557 18.748,18.614 20.847,19.466 L 20.749,19.461 C
|
||||
18.032,21.925 18.249,23.288 18.202,24.301 L 18.202,24.812 C 13.398,21.976
|
||||
8.006,18.283 6.316,19.973 L 5.892,20.99 C 11.546,24.893 17.202,28.796
|
||||
21.852,32.704 C 22.249,33.138 22.902,33.403 23.974,33.385 C 28.907,33.336
|
||||
33.583,32.949 38.07,32.451 L 43.588,34.829 L 43.417,27.782 C 38.887,23.557
|
||||
32.161,21.394 27.091,18.166 C 28.105,17.132 28.732,15.715 28.732,14.156 C
|
||||
28.732,10.992 26.164,8.423 22.998,8.423 C 19.835,8.423 17.266,10.992
|
||||
17.266,14.156 Z M 27.246,14.156 C 27.246,16.496 25.341,18.399 22.998,18.399 C
|
||||
20.655,18.399 18.752,16.496 18.752,14.156 C 18.752,11.813 20.655,9.911
|
||||
22.998,9.911 C 25.341,9.911 27.246,11.813 27.246,14.156 Z M 21.197,11.848 C
|
||||
20.842,12.223 20.608,12.689 20.497,13.249 L 19.885,13.249 L 20.018,13.888 L
|
||||
20.423,13.888 C 20.42,13.954 20.415,14.027 20.415,14.101 C 20.415,14.229
|
||||
20.42,14.338 20.426,14.434 L 19.885,14.434 L 20.018,15.075 L 20.509,15.075 C
|
||||
20.625,15.625 20.854,16.084 21.198,16.452 C 21.721,17.009 22.403,17.285
|
||||
23.253,17.285 C 23.716,17.285 24.118,17.202 24.458,17.036 L 24.216,15.889 C
|
||||
23.98,16.124 23.64,16.24 23.19,16.24 C 22.74,16.24 22.378,16.079 22.101,15.756 C
|
||||
21.943,15.576 21.832,15.347 21.766,15.075 L 24.04,15.075 L 23.905,14.434 L
|
||||
21.683,14.434 C 21.68,14.373 21.679,14.291 21.679,14.192 C 21.679,14.096
|
||||
21.68,13.994 21.686,13.888 L 23.791,13.888 L 23.659,13.249 L 21.772,13.249 C
|
||||
21.842,12.953 21.946,12.721 22.093,12.556 C 22.367,12.23 22.728,12.067
|
||||
23.166,12.067 C 23.694,12.067 24.117,12.23 24.431,12.556 L 24.431,11.29 C
|
||||
24.073,11.11 23.656,11.021 23.179,11.021 C 22.375,11.021 21.713,11.298
|
||||
21.197,11.848 Z M 29.494,23.708 C 30.203,25.42 30.088,27.919 24.739,26.679 C
|
||||
21.778,25.437 21.126,24.068 22.192,22.603 C 22.787,21.692 23.124,20.787
|
||||
23.329,19.878 C 23.931,19.843 24.507,19.716 25.047,19.508 C 26.476,21.08
|
||||
28.884,22.241 29.494,23.708 Z" />
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
@ -21,6 +21,10 @@
|
||||
<group
|
||||
android:checkableBehavior="single"
|
||||
android:id="@+id/further_options">
|
||||
<item
|
||||
android:id="@+id/donation_link"
|
||||
android:title="@string/action_donate"
|
||||
android:icon="@drawable/ic_donate" />
|
||||
<item
|
||||
android:id="@+id/external_changelog"
|
||||
android:title="@string/changelog_full_title" />
|
||||
|
@ -6,6 +6,7 @@
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_debug">Debug</string>
|
||||
<string name="action_quit">Quit</string>
|
||||
<string name="action_donate">Donate</string>
|
||||
<string name="controlcenter_fetch_activity_data">Synchronize</string>
|
||||
<string name="controlcenter_start_sleepmonitor">Sleep Monitor (ALPHA)</string>
|
||||
<string name="controlcenter_find_device">Find lost Device…</string>
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<changelog>
|
||||
<release
|
||||
version="0.20.0"
|
||||
versioncode="98">
|
||||
<release version="0.20.1" versioncode="99">
|
||||
<change>Amazfit Bip: Support icons and text body for notifications</change>
|
||||
<change>Mi Band: Fix setting smart alarms</change>
|
||||
</release>
|
||||
<release version="0.20.0" versioncode="98">
|
||||
<change>Inital Amazfit Bip support (WIP)</change>
|
||||
<change>Various theming fixes</change>
|
||||
<change>Add workaround for blacklist not properly persisting</change>
|
||||
@ -10,7 +12,7 @@
|
||||
<change>Pebble: Pass booleans from Javascript Appmessage correctly</change>
|
||||
<change>Pebble: Make local configuration pages work on most recent webview implementation</change>
|
||||
<change>Pebble: Allow to blacklist calendars</change>
|
||||
<change>Add greek transliteration support</change>
|
||||
<change>Add Greek and German transliteration support</change>
|
||||
<change>Various visual improvements to charts</change>
|
||||
</release>
|
||||
<release version="0.19.4" versioncode="97">
|
||||
|
@ -1,4 +1,4 @@
|
||||
Nutze deine Pebble/Mi Band/Hplus, ohne die proprietäre App des Herstellers, ohne ein Benutzerkonto zu erstellen, und ohne irgendwelche Daten an die Server des Herstellers zu senden.
|
||||
Nutze deine Pebble/Mi Band/Amazfit Bip/Hplus, ohne die proprietäre App des Herstellers, ohne ein Benutzerkonto zu erstellen, und ohne irgendwelche Daten an die Server des Herstellers zu senden.
|
||||
|
||||
Du kannst Benachrichtigungen an deinem Handgelenk erhalten, sowie (je nach Gerät):
|
||||
- Daten der Gerätesensoren sammeln
|
||||
|
2
fastlane/metadata/android/de-DE/short_description.txt
Normal file
2
fastlane/metadata/android/de-DE/short_description.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Nutze deine Pebble/Mi Band/Amazfit Bip/Hplus, ohne die proprietäre App des Herstellers, ohne ein Benutzerkonto zu erstellen, und ohne irgendwelche Daten an die Server des Herstellers zu senden.
|
||||
|
9
fastlane/metadata/android/en-US/changelogs/98.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/98.txt
Normal file
@ -0,0 +1,9 @@
|
||||
* Inital Amazfit Bip support (WIP)
|
||||
* Various theming fixes
|
||||
* Add workaround for blacklist not properly persisting
|
||||
* Handle resetting language to default properly
|
||||
* Pebble: Pass booleans from Javascript Appmessage correctly
|
||||
* Pebble: Make local configuration pages work on most recent webview implementation
|
||||
* Pebble: Allow to blacklist calendars
|
||||
* Add Greek and German transliteration support
|
||||
* Various visual improvements to charts
|
2
fastlane/metadata/android/en-US/changelogs/99.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/99.txt
Normal file
@ -0,0 +1,2 @@
|
||||
* Amazfit Bip: Support icons and text body for notifications
|
||||
* Mi Band: Fix setting smart alarms
|
@ -1,6 +1,7 @@
|
||||
Use your Pebble/Mi Band/Hplus device without the vendor's closed source application and without the need to create an account and transmit any of your data to the vendor's servers.
|
||||
Use your Pebble/Mi Band/Amazfit Bip/Hplus device without the vendor's closed source application and without the need to create an account and transmit any of your data to the vendor's servers.
|
||||
|
||||
You can get notifications on your wrist and (depending on the device):
|
||||
|
||||
- collect data from the device sensors
|
||||
- control music playing on your android device
|
||||
- see the weather
|
||||
|
@ -1 +1 @@
|
||||
Use your Pebble/Mi Band/Hplus device without the vendor's closed source application and without the need to create an account and transmit any of your data to the vendor's servers.
|
||||
Use your Pebble/Mi Band/Amazfit Bip/Hplus device without the vendor's closed source application and without the need to create an account and transmit any of your data to the vendor's servers.
|
||||
|
@ -1,4 +1,4 @@
|
||||
Utilizza il tuo dispositivo Pebble/Mi Band/Hplus senza dipendere dall'applicazione proprietaria del vendor e senza bisogno di creare accounts e trasferire i tuoi dati altrove.
|
||||
Utilizza il tuo dispositivo Pebble/Mi Band/Amazfit Bip/Hplus senza dipendere dall'applicazione proprietaria del vendor e senza bisogno di creare accounts e trasferire i tuoi dati altrove.
|
||||
|
||||
Vedi le notifiche direttamente sul tuo polso, e inoltre (a seconda del dispositivo):
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Utilizza il tuo dispositivo Pebble/Mi Band/Hplus senza dipendere dall'applicazione proprietaria del vendor e senza bisogno di creare accounts e trasferire i tuoi dati altrove.
|
||||
Utilizza il tuo dispositivo Pebble/Mi Band/Amazfit Bip/Hplus senza dipendere dall'applicazione proprietaria del vendor e senza bisogno di creare accounts e trasferire i tuoi dati altrove.
|
||||
|
Loading…
Reference in New Issue
Block a user